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:
8 files changed
+311
-42
No files matched your search
@@ -809,6 +809,168 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) {
|
||||
db_menu_close(db);
|
||||
}
|
||||
|
||||
/* ── per-app context menu ─────────────────────────────────────────────── */
|
||||
|
||||
static int ctx_total_h(void) {
|
||||
return DB_CTX_PAD + DB_CTX_N * DB_CTX_ITEM_H + DB_CTX_PAD;
|
||||
}
|
||||
|
||||
static void db_ctx_draw(Deskbar *db) {
|
||||
if (!db->ctx_win) return;
|
||||
int ww = DB_CTX_W;
|
||||
int wh = ctx_total_h();
|
||||
int mini = (db->ctx_app_idx >= 0 && db->ctx_app_idx < db->napps)
|
||||
? db->apps[db->ctx_app_idx].minimized : 0;
|
||||
|
||||
static const char *labels_normal[] = { "Raise", "Minimize", "Close" };
|
||||
static const char *labels_mini[] = { "Restore", "Restore", "Close" };
|
||||
const char **labels = mini ? labels_mini : labels_normal;
|
||||
|
||||
XftDraw *xd = XftDrawCreate(db->dpy, db->ctx_win, db->visual, db->cmap);
|
||||
|
||||
/* background */
|
||||
XSetForeground(db->dpy, db->gc, db_color(db, 0x2C2C2C));
|
||||
XFillRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww, wh);
|
||||
|
||||
/* 1px border */
|
||||
XSetForeground(db->dpy, db->gc, db_color(db, 0x505050));
|
||||
XDrawRectangle(db->dpy, db->ctx_win, db->gc, 0, 0, ww - 1, wh - 1);
|
||||
|
||||
for (int i = 0; i < DB_CTX_N; i++) {
|
||||
int iy = DB_CTX_PAD + i * DB_CTX_ITEM_H;
|
||||
|
||||
if (i == db->ctx_hov) {
|
||||
XSetForeground(db->dpy, db->gc, db_color(db, 0x4A90D9));
|
||||
XFillRectangle(db->dpy, db->ctx_win, db->gc,
|
||||
2, iy + 1, ww - 4, DB_CTX_ITEM_H - 2);
|
||||
}
|
||||
|
||||
/* "Close" item tinted red */
|
||||
unsigned long tc = (i == db->ctx_hov) ? 0xFFFFFF
|
||||
: (i == DB_CTX_N - 1) ? 0xFF6B6B /* Close = red tint */
|
||||
: 0xE8E8E8;
|
||||
draw_str(db, xd, db->font_ui, tc,
|
||||
14, str_baseline(db->font_ui, iy, DB_CTX_ITEM_H),
|
||||
labels[i], 0, 0);
|
||||
}
|
||||
|
||||
XftDrawDestroy(xd);
|
||||
XFlush(db->dpy);
|
||||
}
|
||||
|
||||
static void db_ctx_close(Deskbar *db) {
|
||||
if (!db->ctx_win) return;
|
||||
XUngrabPointer(db->dpy, CurrentTime);
|
||||
XDestroyWindow(db->dpy, db->ctx_win);
|
||||
db->ctx_win = None;
|
||||
db->ctx_app_idx = -1;
|
||||
db->ctx_hov = -1;
|
||||
}
|
||||
|
||||
static void db_ctx_open(Deskbar *db, int app_idx, int rx, int ry) {
|
||||
db_ctx_close(db);
|
||||
|
||||
int sw = DisplayWidth(db->dpy, db->screen);
|
||||
int sh = DisplayHeight(db->dpy, db->screen);
|
||||
int wh = ctx_total_h();
|
||||
int wx = sw - DB_W - DB_CTX_W - 2;
|
||||
int wy = ry;
|
||||
if (wy + wh > sh) wy = sh - wh - 2;
|
||||
if (wy < 0) wy = 0;
|
||||
|
||||
XSetWindowAttributes swa;
|
||||
swa.override_redirect = True;
|
||||
swa.background_pixel = db_color(db, 0x2C2C2C);
|
||||
swa.event_mask = ExposureMask;
|
||||
db->ctx_win = XCreateWindow(
|
||||
db->dpy, db->root, wx, wy, DB_CTX_W, wh, 1,
|
||||
db->depth, InputOutput, db->visual,
|
||||
CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
|
||||
|
||||
XSetWindowBorder(db->dpy, db->ctx_win, db_color(db, 0x505050));
|
||||
db->ctx_app_idx = app_idx;
|
||||
db->ctx_hov = -1;
|
||||
XMapRaised(db->dpy, db->ctx_win);
|
||||
db_ctx_draw(db);
|
||||
|
||||
XGrabPointer(db->dpy, db->ctx_win, True,
|
||||
ButtonPressMask | PointerMotionMask | ButtonReleaseMask,
|
||||
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
|
||||
}
|
||||
|
||||
static void db_ctx_handle_motion(Deskbar *db, int rx, int ry) {
|
||||
if (!db->ctx_win) return;
|
||||
int cx, cy; unsigned cw, ch, bw, dep; Window cr;
|
||||
XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep);
|
||||
if (rx < cx || ry < cy || rx >= cx + (int)cw || ry >= cy + (int)ch) {
|
||||
if (db->ctx_hov != -1) { db->ctx_hov = -1; db_ctx_draw(db); }
|
||||
return;
|
||||
}
|
||||
int lx = ry - cy - DB_CTX_PAD;
|
||||
int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1;
|
||||
if (idx >= DB_CTX_N) idx = -1;
|
||||
if (idx != db->ctx_hov) { db->ctx_hov = idx; db_ctx_draw(db); }
|
||||
}
|
||||
|
||||
static void db_ctx_handle_press(Deskbar *db, int rx, int ry) {
|
||||
if (!db->ctx_win) return;
|
||||
int cx, cy; unsigned cw, ch, bw, dep; Window cr;
|
||||
XGetGeometry(db->dpy, db->ctx_win, &cr, &cx, &cy, &cw, &ch, &bw, &dep);
|
||||
|
||||
int app_idx = db->ctx_app_idx;
|
||||
int in_menu = (rx >= cx && ry >= cy && rx < cx + (int)cw && ry < cy + (int)ch);
|
||||
|
||||
if (!in_menu || app_idx < 0 || app_idx >= db->napps) {
|
||||
db_ctx_close(db);
|
||||
return;
|
||||
}
|
||||
|
||||
int lx = ry - cy - DB_CTX_PAD;
|
||||
int idx = (lx >= 0) ? lx / DB_CTX_ITEM_H : -1;
|
||||
Window target = db->apps[app_idx].win;
|
||||
int mini = db->apps[app_idx].minimized;
|
||||
db_ctx_close(db);
|
||||
|
||||
if (idx < 0 || idx >= DB_CTX_N) return;
|
||||
|
||||
XEvent ev = {0};
|
||||
ev.xclient.type = ClientMessage;
|
||||
ev.xclient.format = 32;
|
||||
|
||||
if (idx == 0) {
|
||||
/* Raise / Restore */
|
||||
ev.xclient.window = target;
|
||||
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False);
|
||||
ev.xclient.data.l[0] = 2;
|
||||
ev.xclient.data.l[1] = CurrentTime;
|
||||
XSendEvent(db->dpy, db->root, False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
|
||||
} else if (idx == 1) {
|
||||
if (mini) {
|
||||
/* Restore */
|
||||
ev.xclient.window = target;
|
||||
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False);
|
||||
ev.xclient.data.l[0] = 2;
|
||||
ev.xclient.data.l[1] = CurrentTime;
|
||||
} else {
|
||||
/* Minimize via WM_CHANGE_STATE IconicState */
|
||||
ev.xclient.window = target;
|
||||
ev.xclient.message_type = XInternAtom(db->dpy, "WM_CHANGE_STATE", False);
|
||||
ev.xclient.data.l[0] = 3; /* IconicState */
|
||||
}
|
||||
XSendEvent(db->dpy, db->root, False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
|
||||
} else if (idx == 2) {
|
||||
/* Close via _NET_CLOSE_WINDOW */
|
||||
ev.xclient.window = target;
|
||||
ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CLOSE_WINDOW", False);
|
||||
ev.xclient.data.l[0] = CurrentTime;
|
||||
ev.xclient.data.l[1] = 2;
|
||||
XSendEvent(db->dpy, db->root, False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &ev);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── input ────────────────────────────────────────────────────────────── */
|
||||
|
||||
void db_hover(Deskbar *db, int x, int y) {
|
||||
@@ -981,6 +1143,26 @@ void db_run(Deskbar *db) {
|
||||
XEvent e;
|
||||
XNextEvent(db->dpy, &e);
|
||||
|
||||
/* While the per-app context menu is open, the pointer is grabbed.
|
||||
Route motion and button events through the ctx handlers. */
|
||||
if (db->ctx_win) {
|
||||
if (e.type == MotionNotify) {
|
||||
db_ctx_handle_motion(db, e.xmotion.x_root, e.xmotion.y_root);
|
||||
continue;
|
||||
}
|
||||
if (e.type == ButtonPress) {
|
||||
db_ctx_handle_press(db, e.xbutton.x_root, e.xbutton.y_root);
|
||||
continue;
|
||||
}
|
||||
if (e.type == ButtonRelease)
|
||||
continue;
|
||||
if (e.type == Expose && e.xexpose.count == 0) {
|
||||
if (e.xany.window == db->ctx_win) db_ctx_draw(db);
|
||||
else if (e.xany.window == db->win) db_draw(db);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* While the fly-out menu is open, the pointer is grabbed.
|
||||
Route all motion and button events through the menu handlers
|
||||
(they use root-relative coords to determine which panel was hit). */
|
||||
@@ -1026,6 +1208,18 @@ void db_run(Deskbar *db) {
|
||||
system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &");
|
||||
break;
|
||||
}
|
||||
if (e.xbutton.button == Button3) {
|
||||
/* right-click on an app row → open context menu */
|
||||
int ay = db_apps_y();
|
||||
int by = e.xbutton.y;
|
||||
if (by >= ay && by < ay + db->napps * DB_ITEM_H) {
|
||||
int idx = (by - ay) / DB_ITEM_H;
|
||||
if (idx >= 0 && idx < db->napps)
|
||||
db_ctx_open(db, idx,
|
||||
e.xbutton.x_root, e.xbutton.y_root);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e.xbutton.button == Button1 && e.xbutton.y >= sy)
|
||||
system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &");
|
||||
else
|
||||
|
||||
Reference in new issue
Block a user