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

+11
View File
@@ -44,6 +44,12 @@
#define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */ #define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */
#define DB_MAX_APPS 32 #define DB_MAX_APPS 32
/* ── per-app context menu ────────────────────────────────────────────── */
#define DB_CTX_W 140 /* context menu width */
#define DB_CTX_ITEM_H 26 /* item row height */
#define DB_CTX_PAD 4 /* top/bottom padding */
#define DB_CTX_N 3 /* number of items: Raise, Minimize, Close */
/* ── fly-out menu ────────────────────────────────────────────────────── */ /* ── fly-out menu ────────────────────────────────────────────────────── */
#define DB_FLYOUT_W 196 /* fly-out menu panel width */ #define DB_FLYOUT_W 196 /* fly-out menu panel width */
#define DB_FLYOUT_IH 28 /* item row height */ #define DB_FLYOUT_IH 28 /* item row height */
@@ -139,6 +145,11 @@ typedef struct Deskbar {
int sub_hov; /* hovered item in sub-menu */ int sub_hov; /* hovered item in sub-menu */
int sw_cache; /* cached screen width for menu positioning */ int sw_cache; /* cached screen width for menu positioning */
/* per-app right-click context menu */
Window ctx_win; /* None when closed */
int ctx_app_idx; /* which app row the ctx menu is for */
int ctx_hov; /* hovered item index, -1 = none */
/* system tray (freedesktop.org XEmbed tray protocol) */ /* system tray (freedesktop.org XEmbed tray protocol) */
Window tray_icons[DB_TRAY_MAX]; Window tray_icons[DB_TRAY_MAX];
int n_tray_icons; int n_tray_icons;
+194
View File
@@ -809,6 +809,168 @@ static void handle_menu_press(Deskbar *db, int rx, int ry) {
db_menu_close(db); 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 ────────────────────────────────────────────────────────────── */ /* ── input ────────────────────────────────────────────────────────────── */
void db_hover(Deskbar *db, int x, int y) { void db_hover(Deskbar *db, int x, int y) {
@@ -981,6 +1143,26 @@ void db_run(Deskbar *db) {
XEvent e; XEvent e;
XNextEvent(db->dpy, &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. /* While the fly-out menu is open, the pointer is grabbed.
Route all motion and button events through the menu handlers Route all motion and button events through the menu handlers
(they use root-relative coords to determine which panel was hit). */ (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 &"); system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &");
break; 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) if (e.xbutton.button == Button1 && e.xbutton.y >= sy)
system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &"); system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &");
else else
+25 -20
View File
@@ -1,22 +1,29 @@
# OrionOS picom compositor config # OrionOS picom compositor config — macOS-inspired
# Goal: subtle window shadows, no transparency, fast rendering. # Rounded corners, soft macOS-style shadows, fast fading.
# BeOS/OrionOS didn't have transparency — we keep it clean.
backend = "xrender"; # xrender works on all hardware; switch to glx for performance backend = "xrender";
vsync = false; vsync = false;
# ── rounded corners ───────────────────────────────────────────────────────
corner-radius = 10;
rounded-corners-exclude = [
"_NET_WM_WINDOW_TYPE@:a *= 'DOCK'",
"_NET_WM_WINDOW_TYPE@:a *= 'DESKTOP'",
"_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'",
"_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'",
"override_redirect = 1 && !WM_CLASS@:s"
];
# ── shadows ─────────────────────────────────────────────────────────────── # ── shadows ───────────────────────────────────────────────────────────────
# macOS-style: larger, softer, offset down-right
shadow = true; shadow = true;
shadow-radius = 8; shadow-radius = 18;
shadow-offset-x = 4; shadow-offset-x = 0;
shadow-offset-y = 4; shadow-offset-y = 8;
shadow-opacity = 0.30; shadow-opacity = 0.35;
shadow-color = "#000000"; shadow-color = "#000000";
# Don't shadow the deskbar, menus, or tooltips
shadow-exclude = [ shadow-exclude = [
"class_g = 'skydeskbar'",
"class_g = 'skylauncher'",
"_NET_WM_WINDOW_TYPE@:a *= 'DOCK'", "_NET_WM_WINDOW_TYPE@:a *= 'DOCK'",
"_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'", "_NET_WM_WINDOW_TYPE@:a *= 'TOOLTIP'",
"_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'", "_NET_WM_WINDOW_TYPE@:a *= 'NOTIFICATION'",
@@ -27,20 +34,18 @@ shadow-exclude = [
# ── fading ──────────────────────────────────────────────────────────────── # ── fading ────────────────────────────────────────────────────────────────
fading = true; fading = true;
fade-in-step = 0.06; fade-in-step = 0.08;
fade-out-step = 0.06; fade-out-step = 0.08;
fade-delta = 8; # ms between steps — snappy, not sluggish fade-delta = 6;
no-fading-openclose = false; no-fading-openclose = false;
# ── opacity ─────────────────────────────────────────────────────────────── # ── opacity ───────────────────────────────────────────────────────────────
# No transparency — BeOS aesthetic is solid, crisp windows.
inactive-opacity = 1.0; inactive-opacity = 1.0;
active-opacity = 1.0; active-opacity = 1.0;
frame-opacity = 1.0; frame-opacity = 1.0;
inactive-opacity-override = false; inactive-opacity-override = false;
# ── blur ───────────────────────────────────────────────────────────────── # ── blur ─────────────────────────────────────────────────────────────────
# Disabled — keep it clean.
blur-method = "none"; blur-method = "none";
# ── focus & refresh ─────────────────────────────────────────────────────── # ── focus & refresh ───────────────────────────────────────────────────────
@@ -55,9 +60,9 @@ mark-ovredir-focused = false;
wintypes: wintypes:
{ {
tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; }; tooltip = { fade = true; shadow = false; opacity = 1.0; focus = true; };
dock = { shadow = false; clip-shadow-above = true; }; dock = { shadow = false; clip-shadow-above = true; corner-radius = 0; };
dnd = { shadow = false; }; dnd = { shadow = false; };
popup_menu = { opacity = 1.0; shadow = false; }; popup_menu = { opacity = 1.0; shadow = false; corner-radius = 8; };
dropdown_menu = { opacity = 1.0; shadow = false; }; dropdown_menu = { opacity = 1.0; shadow = false; corner-radius = 8; };
notification = { shadow = false; }; notification = { shadow = false; corner-radius = 8; };
}; };
+3
View File
@@ -29,6 +29,7 @@
#define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */ #define COLOR_SNAP_OVERLAY 0x4A90D9 /* macOS blue snap highlight */
/* ── geometry ────────────────────────────────────────────────────────── */ /* ── geometry ────────────────────────────────────────────────────────── */
#define DESKBAR_W 200 /* must match DB_W in deskbar/include/deskbar.h */
#define TITLE_HEIGHT 28 /* macOS-style height */ #define TITLE_HEIGHT 28 /* macOS-style height */
#define TITLE_TAB_W 200 #define TITLE_TAB_W 200
#define BORDER_W 1 /* thin 1px macOS-style border */ #define BORDER_W 1 /* thin 1px macOS-style border */
@@ -130,6 +131,7 @@ typedef struct WM {
int swl_count; int swl_count;
Window swl_wins[64]; Window swl_wins[64];
char swl_titles[64][128]; char swl_titles[64][128];
int swl_mini[64]; /* 1 if corresponding entry is minimized */
} WM; } WM;
/* core */ /* core */
@@ -175,6 +177,7 @@ int menu_item_at(int py);
/* switcher */ /* switcher */
void switcher_show(WM *wm); void switcher_show(WM *wm);
void switcher_next(WM *wm); void switcher_next(WM *wm);
void switcher_prev(WM *wm);
void switcher_hide(WM *wm, int commit); void switcher_hide(WM *wm, int commit);
void switcher_draw(WM *wm); 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->x = wa.x;
c->y = wa.y; c->y = wa.y;
} }
/* never land behind deskbar (top-right panel ~180px wide) */ /* never land behind deskbar */
int sw = DisplayWidth(wm->dpy, wm->screen); 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; c->y = 40;
/* fetch title */ /* fetch title */
@@ -228,7 +228,7 @@ void client_maximize(WM *wm, Client *c) {
int sh = DisplayHeight(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen);
/* leave room for deskbar on the right */ /* 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_w = sw - deskbar_w - BORDER_W * 2;
int new_h = sh - TITLE_HEIGHT - BORDER_W; 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); if (c->maximized) client_unmaximize(wm, c);
int sw = DisplayWidth(wm->dpy, wm->screen); int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(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; int half_w = (sw - deskbar_w) / 2;
c->x = 0; c->x = 0;
@@ -299,7 +299,7 @@ void client_snap_right(WM *wm, Client *c) {
if (c->maximized) client_unmaximize(wm, c); if (c->maximized) client_unmaximize(wm, c);
int sw = DisplayWidth(wm->dpy, wm->screen); int sw = DisplayWidth(wm->dpy, wm->screen);
int sh = DisplayHeight(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; int half_w = (sw - deskbar_w) / 2;
c->x = half_w; c->x = half_w;
+38 -5
View File
@@ -33,7 +33,7 @@ static void check_snap(WM *wm, Client *c) {
} else { } else {
client_snap_left(wm, c); 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); client_snap_right(wm, c);
} else if (c->y <= SNAP_DIST) { } else if (c->y <= SNAP_DIST) {
client_maximize(wm, c); client_maximize(wm, c);
@@ -95,10 +95,34 @@ void on_enter_notify(WM *wm, XCrossingEvent *e) {
} }
void on_client_message(WM *wm, XClientMessageEvent *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) { if (e->message_type == net_active) {
Client *c = client_find_by_win(wm, e->window); Client *c = client_find_by_win(wm, e->window);
if (c) client_restore(wm, c); 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) if ((e->state & Mod1Mask) && sym == XK_F4 && wm->focused)
send_delete(wm, wm->focused->win); 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 ((e->state & Mod1Mask) && sym == XK_Tab) {
if (wm->swl_visible) switcher_next(wm); int backward = (e->state & ShiftMask) != 0;
else if (wm->clients) switcher_show(wm); 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; return;
} }
+34 -12
View File
@@ -8,17 +8,19 @@
static void swl_populate(WM *wm) { static void swl_populate(WM *wm) {
wm->swl_count = 0; wm->swl_count = 0;
/* current focused window goes first */ /* current focused (non-minimized) window goes first */
if (wm->focused && !wm->focused->minimized) { 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); strncpy(wm->swl_titles[0], wm->focused->title, 127);
wm->swl_titles[0][127] = '\0'; wm->swl_titles[0][127] = '\0';
wm->swl_count = 1; wm->swl_count = 1;
} }
/* all other windows, including minimized ones */
for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) { for (Client *c = wm->clients; c && wm->swl_count < 64; c = c->next) {
if (c->minimized) continue;
if (wm->focused && c == wm->focused) 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); strncpy(wm->swl_titles[wm->swl_count], c->title, 127);
wm->swl_titles[wm->swl_count][127] = '\0'; wm->swl_titles[wm->swl_count][127] = '\0';
wm->swl_count++; wm->swl_count++;
@@ -61,29 +63,34 @@ void switcher_draw(WM *wm) {
for (int vi = 0; vi < vis; vi++) { for (int vi = 0; vi < vis; vi++) {
int i = vstart + vi; int i = vstart + vi;
int selected = (i == wm->swl_idx); int selected = (i == wm->swl_idx);
int mini = wm->swl_mini[i];
int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD); int ix = SWL_PAD + vi * (SWL_ITEM_W + SWL_PAD);
int iy = 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); XSetForeground(wm->dpy, wm->gc, card_bg);
XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H); XFillRectangle(wm->dpy, wm->swl_win, wm->gc, ix, iy, SWL_ITEM_W, SWL_ITEM_H);
XSetForeground(wm->dpy, wm->gc, 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); 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_x = ix + (SWL_ITEM_W - SWL_ICON_SZ) / 2;
int icon_y = iy + 8; int icon_y = iy + 8;
XSetForeground(wm->dpy, wm->gc, 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); 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' }; char letter[2] = { wm->swl_titles[i][0] ? wm->swl_titles[i][0] : '?', '\0' };
if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; 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; 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; XGlyphInfo ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext); XftTextExtentsUtf8(wm->dpy, wm->font_title, (const FcChar8 *)letter, 1, &ext);
int lx = icon_x + (SWL_ICON_SZ - (int)ext.width) / 2; 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); XftDrawStringUtf8(xd, &icon_col, wm->font_title, lx, ly, (const FcChar8 *)letter, 1);
XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col); XftColorFree(wm->dpy, wm->visual, wm->cmap, &icon_col);
/* title below icon */ /* title below icon — prefix [..] for minimized */
const char *title = wm->swl_titles[i]; 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; 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; XGlyphInfo text_ext;
XftTextExtentsUtf8(wm->dpy, wm->font_title, XftTextExtentsUtf8(wm->dpy, wm->font_title,
(const FcChar8 *)title, strlen(title), &text_ext); (const FcChar8 *)title, strlen(title), &text_ext);
@@ -163,6 +179,12 @@ void switcher_next(WM *wm) {
switcher_draw(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) { void switcher_hide(WM *wm, int commit) {
if (!wm->swl_visible) return; if (!wm->swl_visible) return;
XUngrabKeyboard(wm->dpy, CurrentTime); XUngrabKeyboard(wm->dpy, CurrentTime);
+1
View File
@@ -90,6 +90,7 @@ void wm_init(WM *wm) {
/* global key grabs */ /* global key grabs */
struct { KeySym sym; unsigned mod; } grabs[] = { struct { KeySym sym; unsigned mod; } grabs[] = {
{ XK_Tab, Mod1Mask }, { XK_Tab, Mod1Mask },
{ XK_Tab, Mod1Mask | ShiftMask }, /* Shift+Alt+Tab — backwards */
{ XK_F4, Mod1Mask }, { XK_F4, Mod1Mask },
{ XK_F9, Mod1Mask }, /* minimize */ { XK_F9, Mod1Mask }, /* minimize */
{ XK_F10, Mod1Mask }, /* maximize */ { XK_F10, Mod1Mask }, /* maximize */