[hackers] [PATCH] Add titlebars to dwm

From: Petr Derbyshev <drvpotr_AT_gmail.com>
Date: Sun, 23 Aug 2026 10:43:39 +0500

---
 config.def.h |  31 +++-
 dwm.c        | 414 ++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 422 insertions(+), 23 deletions(-)
diff --git a/config.def.h b/config.def.h
index 81c3fc0..2353c59 100644
--- a/config.def.h
+++ b/config.def.h
_AT_@ -13,9 +13,28 @@ static const char col_gray3[]       = "#bbbbbb";
 static const char col_gray4[]       = "#eeeeee";
 static const char col_cyan[]        = "#005577";
 static const char *colors[][3]      = {
-	/*               fg         bg         border   */
-	[SchemeNorm] = { col_gray3, col_gray1, col_gray2 },
-	[SchemeSel]  = { col_gray4, col_cyan,  col_cyan  },
+	/*                    fg         bg         border   */
+	[SchemeNorm]      = { col_gray3, col_gray1, col_gray2 },
+	[SchemeSel]       = { col_gray4, col_cyan,  col_cyan  },
+	/* titlebars reuse the bar's fonts and follow its colors by default; change
+	 * these two lines to theme them apart. "border" is the frame border drawn
+	 * around the titlebar, keep it equal to the two lines above */
+	[SchemeTitleNorm] = { col_gray3, col_gray1, col_gray2 },
+	[SchemeTitleSel]  = { col_gray4, col_cyan,  col_cyan  },
+};
+
+/* titlebar */
+static const int showtitlebar   = 1;  /* 0 means no titlebars */
+static const int titlebarheight = 0;  /* 0 means "as tall as the bar" */
+static const int centertitle    = 0;  /* 1 centers the window title */
+static const int dragthreshold  = 8;
+/* buttons drawn in the right corner of every titlebar, left to right; the
+ * labels are plain strings, so any glyph your font provides will do */
+static const TitleBtn titlebarbtns[] = {
+	/* label       function         argument */
+	{ "\u2013",    hide,      {0} }, /* minimise, MODKEY+Shift+h restores it */
+	{ "\u25a1",    togglefloating,  {0} },
+	{ "\u2715",    killclient,      {0} },
 };
 
 /* tagging */
_AT_@ -75,6 +94,7 @@ static const Key keys[] = {
 	{ MODKEY,                       XK_Return, zoom,           {0} },
 	{ MODKEY,                       XK_Tab,    view,           {0} },
 	{ MODKEY|ShiftMask,             XK_c,      killclient,     {0} },
+	{ MODKEY|ShiftMask,             XK_h,      showhidden,     {0} },
 	{ MODKEY,                       XK_t,      setlayout,      {.v = &layouts[0]} },
 	{ MODKEY,                       XK_f,      setlayout,      {.v = &layouts[1]} },
 	{ MODKEY,                       XK_m,      setlayout,      {.v = &layouts[2]} },
_AT_@ -99,12 +119,15 @@ static const Key keys[] = {
 };
 
 /* button definitions */
-/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */
+/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkTitleBar,
+ * ClkClientWin, or ClkRootWin */
 static const Button buttons[] = {
 	/* click                event mask      button          function        argument */
 	{ ClkLtSymbol,          0,              Button1,        setlayout,      {0} },
 	{ ClkLtSymbol,          0,              Button3,        setlayout,      {.v = &layouts[2]} },
 	{ ClkWinTitle,          0,              Button2,        zoom,           {0} },
+	{ ClkTitleBar,          0,              Button1,        dragtitle,      {0} },
+	{ ClkTitleBar,          0,              Button3,        togglefloating, {0} },
 	{ ClkStatusText,        0,              Button2,        spawn,          {.v = termcmd } },
 	{ ClkClientWin,         MODKEY,         Button1,        movemouse,      {0} },
 	{ ClkClientWin,         MODKEY,         Button2,        togglefloating, {0} },
diff --git a/dwm.c b/dwm.c
index ab3a84c..24dceba 100644
--- a/dwm.c
+++ b/dwm.c
_AT_@ -49,7 +49,7 @@
 #define CLEANMASK(mask)         (mask & ~(numlockmask|LockMask) & (ShiftMask|ControlMask|Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|Mod5Mask))
 #define INTERSECT(x,y,w,h,m)    (MAX(0, MIN((x)+(w),(m)->wx+(m)->ww) - MAX((x),(m)->wx)) \
                                * MAX(0, MIN((y)+(h),(m)->wy+(m)->wh) - MAX((y),(m)->wy)))
-#define ISVISIBLE(C)            ((C->tags & C->mon->tagset[C->mon->seltags]))
+#define ISVISIBLE(C)            ((C)->tags & (C)->mon->tagset[(C)->mon->seltags] && !(C)->ishidden)
 #define MOUSEMASK               (BUTTONMASK|PointerMotionMask)
 #define WIDTH(X)                ((X)->w + 2 * (X)->bw)
 #define HEIGHT(X)               ((X)->h + 2 * (X)->bw)
_AT_@ -58,13 +58,13 @@
 
 /* enums */
 enum { CurNormal, CurResize, CurMove, CurLast }; /* cursor */
-enum { SchemeNorm, SchemeSel }; /* color schemes */
+enum { SchemeNorm, SchemeSel, SchemeTitleNorm, SchemeTitleSel }; /* color schemes */
 enum { NetSupported, NetWMName, NetWMState, NetWMCheck,
        NetWMFullscreen, NetActiveWindow, NetWMWindowType,
        NetWMWindowTypeDialog, NetClientList, NetLast }; /* EWMH atoms */
 enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */
 enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle,
-       ClkClientWin, ClkRootWin, ClkLast }; /* clicks */
+       ClkClientWin, ClkTitleBar, ClkRootWin, ClkLast }; /* clicks */
 
 typedef union {
 	int i;
_AT_@ -81,6 +81,12 @@ typedef struct {
 	const Arg arg;
 } Button;
 
+typedef struct {
+	const char *label;
+	void (*func)(const Arg *arg);
+	const Arg arg;
+} TitleBtn; /* a button drawn in the right corner of every titlebar */
+
 typedef struct Monitor Monitor;
 typedef struct Client Client;
 struct Client {
_AT_@ -92,10 +98,13 @@ struct Client {
 	int bw, oldbw;
 	unsigned int tags;
 	int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen;
+	int ishidden;    /* minimised: kept off-screen and skipped by ISVISIBLE */
+	int hoverbtn;    /* titlebar button below the pointer, -1 for none */
 	Client *next;
 	Client *snext;
 	Monitor *mon;
 	Window win;
+	Window titlebar; /* decoration, drawn in the top strip of the frame */
 };
 
 typedef struct {
_AT_@ -149,6 +158,7 @@ static void attach(Client *c);
 static void attachstack(Client *c);
 static void buttonpress(XEvent *e);
 static void checkotherwm(void);
+static Client *clientat(Monitor *m, int x, int y);
 static void cleanup(void);
 static void cleanupmon(Monitor *mon);
 static void clientmessage(XEvent *e);
_AT_@ -156,12 +166,17 @@ static void configure(Client *c);
 static void configurenotify(XEvent *e);
 static void configurerequest(XEvent *e);
 static Monitor *createmon(void);
+static void createtitlebar(Client *c);
 static void destroynotify(XEvent *e);
 static void detach(Client *c);
 static void detachstack(Client *c);
 static Monitor *dirtomon(int dir);
+static void dragtitle(const Arg *arg);
 static void drawbar(Monitor *m);
 static void drawbars(void);
+static void drawtitlebar(Client *c);
+static void drawtitlebars(void);
+static int dragstarted(int x, int y, XEvent *ev);
 static void enternotify(XEvent *e);
 static void expose(XEvent *e);
 static void focus(Client *c);
_AT_@ -174,9 +189,11 @@ static long getstate(Window w);
 static int gettextprop(Window w, Atom atom, char *text, unsigned int size);
 static void grabbuttons(Client *c, int focused);
 static void grabkeys(void);
+static void hide(const Arg *arg);
 static void incnmaster(const Arg *arg);
 static void keypress(XEvent *e);
 static void killclient(const Arg *arg);
+static void leavenotify(XEvent *e);
 static void manage(Window w, XWindowAttributes *wa);
 static void mappingnotify(XEvent *e);
 static void maprequest(XEvent *e);
_AT_@ -204,10 +221,17 @@ static void setmfact(const Arg *arg);
 static void setup(void);
 static void seturgent(Client *c, int urg);
 static void showhide(Client *c);
+static void showhidden(const Arg *arg);
 static void spawn(const Arg *arg);
+static void stacktitlebar(Client *c);
+static void swapclients(Monitor *m, Client *a, Client *b);
 static void tag(const Arg *arg);
 static void tagmon(const Arg *arg);
 static void tile(Monitor *m);
+static Client *titlebartoclient(Window w);
+static int titlebtnat(Client *c, int x);
+static int titlebtnsw(void);
+static int titleh(Client *c);
 static void togglebar(const Arg *arg);
 static void togglefloating(const Arg *arg);
 static void toggletag(const Arg *arg);
_AT_@ -239,6 +263,7 @@ static char stext[256];
 static int screen;
 static int sw, sh;           /* X display screen geometry width, height */
 static int bh;               /* bar height */
+static int th;               /* titlebar height */
 static int lrpad;            /* sum of left and right padding for text */
 static int (*xerrorxlib)(Display *, XErrorEvent *);
 static unsigned int numlockmask = 0;
_AT_@ -252,6 +277,7 @@ static void (*handler[LASTEvent]) (XEvent *) = {
 	[Expose] = expose,
 	[FocusIn] = focusin,
 	[KeyPress] = keypress,
+	[LeaveNotify] = leavenotify,
 	[MappingNotify] = mappingnotify,
 	[MapRequest] = maprequest,
 	[MotionNotify] = motionnotify,
_AT_@ -338,13 +364,15 @@ applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact)
 		if (*y + *h + 2 * c->bw <= m->wy)
 			*y = m->wy;
 	}
-	if (*h < bh)
-		*h = bh;
+	if (*h < bh + titleh(c))
+		*h = bh + titleh(c);
 	if (*w < bh)
 		*w = bh;
 	if (resizehints || c->isfloating || !c->mon->lt[c->mon->sellt]->arrange) {
 		if (!c->hintsvalid)
 			updatesizehints(c);
+		/* the hints describe the managed window, the titlebar is not part of it */
+		*h -= titleh(c);
 		/* see last two sentences in ICCCM 4.1.2.3 */
 		baseismin = c->basew == c->minw && c->baseh == c->minh;
 		if (!baseismin) { /* temporarily remove base dimensions */
_AT_@ -374,10 +402,33 @@ applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact)
 			*w = MIN(*w, c->maxw);
 		if (c->maxh)
 			*h = MIN(*h, c->maxh);
+		*h += titleh(c);
 	}
 	return *x != c->x || *y != c->y || *w != c->w || *h != c->h;
 }
 
+int
+dragstarted(int x, int y, XEvent *ev)
+{
+	for (;;) {
+		XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, ev);
+		switch (ev->type) {
+		case ConfigureRequest:
+		case Expose:
+		case MapRequest:
+			handler[ev->type](ev);
+			break;
+		case MotionNotify:
+			if (abs(ev->xmotion.x_root - x) > dragthreshold
+			|| abs(ev->xmotion.y_root - y) > dragthreshold)
+				return 1;
+			break;
+		case ButtonRelease:
+			return 0;
+		}
+	}
+}
+
 void
 arrange(Monitor *m)
 {
_AT_@ -417,6 +468,7 @@ attachstack(Client *c)
 void
 buttonpress(XEvent *e)
 {
+	int btn;
 	unsigned int i, x, click;
 	Arg arg = {0};
 	Client *c;
_AT_@ -444,6 +496,15 @@ buttonpress(XEvent *e)
 			click = ClkStatusText;
 		else
 			click = ClkWinTitle;
+	} else if ((c = titlebartoclient(ev->window))) {
+		focus(c);
+		restack(selmon);
+		if (ev->button == Button1 && (btn = titlebtnat(c, ev->x)) >= 0) {
+			if (titlebarbtns[btn].func)
+				titlebarbtns[btn].func(&titlebarbtns[btn].arg);
+			return;
+		}
+		click = ClkTitleBar;
 	} else if ((c = wintoclient(ev->window))) {
 		focus(c);
 		restack(selmon);
_AT_@ -511,6 +572,20 @@ cleanupmon(Monitor *mon)
 	free(mon);
 }
 
+/* the visible tiled client covering (x, y), NULL if there is none */
+Client *
+clientat(Monitor *m, int x, int y)
+{
+	Client *c;
+
+	for (c = m->clients; c; c = c->next)
+		if (ISVISIBLE(c) && !c->isfloating
+		&& x >= c->x && x < c->x + WIDTH(c)
+		&& y >= c->y && y < c->y + HEIGHT(c))
+			return c;
+	return NULL;
+}
+
 void
 clientmessage(XEvent *e)
 {
_AT_@ -540,9 +615,9 @@ configure(Client *c)
 	ce.event = c->win;
 	ce.window = c->win;
 	ce.x = c->x;
-	ce.y = c->y;
+	ce.y = c->y + titleh(c);
 	ce.width = c->w;
-	ce.height = c->h;
+	ce.height = MAX(c->h - titleh(c), 1);
 	ce.border_width = c->bw;
 	ce.above = None;
 	ce.override_redirect = False;
_AT_@ -563,7 +638,7 @@ configurenotify(XEvent *e)
 		sw = ev->width;
 		sh = ev->height;
 		if (updategeom() || dirty) {
-			drw_resize(drw, sw, bh);
+			drw_resize(drw, sw, MAX(bh, th));
 			updatebars();
 			for (m = mons; m; m = m->next) {
 				for (c = m->clients; c; c = c->next)
_AT_@ -604,7 +679,7 @@ configurerequest(XEvent *e)
 			}
 			if (ev->value_mask & CWHeight) {
 				c->oldh = c->h;
-				c->h = ev->height;
+				c->h = ev->height + titleh(c);
 			}
 			if ((c->x + c->w) > m->mx + m->mw && c->isfloating)
 				c->x = m->mx + (m->mw / 2 - WIDTH(c) / 2); /* center in x direction */
_AT_@ -613,7 +688,7 @@ configurerequest(XEvent *e)
 			if ((ev->value_mask & (CWX|CWY)) && !(ev->value_mask & (CWWidth|CWHeight)))
 				configure(c);
 			if (ISVISIBLE(c))
-				XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h);
+				resizeclient(c, c->x, c->y, c->w, c->h);
 		} else
 			configure(c);
 	} else {
_AT_@ -646,6 +721,30 @@ createmon(void)
 	return m;
 }
 
+void
+createtitlebar(Client *c)
+{
+	XSetWindowAttributes wa = {
+		.override_redirect = True,
+		.background_pixel = 0,
+		.border_pixel = 0,
+		.event_mask = ButtonPressMask|ExposureMask|PointerMotionMask
+		              |EnterWindowMask|LeaveWindowMask
+	};
+	XClassHint ch = {"dwm", "dwm"};
+
+	c->hoverbtn = -1;
+	if (!showtitlebar || c->titlebar)
+		return;
+	wa.background_pixel = scheme[SchemeTitleNorm][ColBg].pixel;
+	wa.border_pixel = scheme[SchemeNorm][ColBorder].pixel;
+	c->titlebar = XCreateWindow(dpy, root, c->x, c->y, MAX(c->w, 1), th, c->bw,
+		DefaultDepth(dpy, screen), CopyFromParent, DefaultVisual(dpy, screen),
+		CWOverrideRedirect|CWBackPixel|CWBorderPixel|CWEventMask, &wa);
+	XDefineCursor(dpy, c->titlebar, cursor[CurNormal]->cursor);
+	XSetClassHint(dpy, c->titlebar, &ch);
+}
+
 void
 destroynotify(XEvent *e)
 {
_AT_@ -694,6 +793,63 @@ dirtomon(int dir)
 	return m;
 }
 
+/* move a window by its titlebar: floating clients follow the pointer, tiled
+ * ones swap places with whatever tile they are dragged over. A plain click
+ * (no drag) promotes the client to master, like MODKEY+Return does. */
+void
+dragtitle(const Arg *arg)
+{
+	Client *c, *t;
+	Monitor *m;
+	XEvent ev;
+	Time lasttime = 0;
+	int x, y = 0;
+
+	if (!(c = selmon->sel) || c->isfullscreen)
+		return;
+	if (!getrootptr(&x, &y))
+		return;
+	restack(selmon);
+	if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync,
+		None, cursor[CurMove]->cursor, CurrentTime) != GrabSuccess)
+		return;
+	if (!dragstarted(x, y, &ev)) {
+		XUngrabPointer(dpy, CurrentTime);
+		return;
+	}
+	if (c->isfloating || !selmon->lt[selmon->sellt]->arrange) {
+		movemouse(arg);
+		return;
+	}
+	do {
+		XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev);
+		switch(ev.type) {
+		case ConfigureRequest:
+		case Expose:
+		case MapRequest:
+			handler[ev.type](&ev);
+			break;
+		case MotionNotify:
+			if ((ev.xmotion.time - lasttime) <= (1000 / refreshrate))
+				continue;
+			lasttime = ev.xmotion.time;
+
+			if ((t = clientat(selmon, ev.xmotion.x_root, ev.xmotion.y_root))
+			&& t != c) {
+				swapclients(selmon, c, t);
+				arrange(selmon);
+			}
+			break;
+		}
+	} while (ev.type != ButtonRelease);
+	XUngrabPointer(dpy, CurrentTime);
+	if ((m = recttomon(ev.xbutton.x_root, ev.xbutton.y_root, 1, 1)) != selmon) {
+		sendmon(c, m);
+		selmon = m;
+		focus(NULL);
+	}
+}
+
 void
 drawbar(Monitor *m)
 {
_AT_@ -756,6 +912,50 @@ drawbars(void)
 		drawbar(m);
 }
 
+void
+drawtitlebar(Client *c)
+{
+	unsigned int i;
+	int x, w, bw, tw, lpad, scm;
+
+	if (!titleh(c) || !ISVISIBLE(c))
+		return;
+	scm = (c == c->mon->sel && c->mon == selmon) ? SchemeTitleSel : SchemeTitleNorm;
+	XSetWindowBorder(dpy, c->titlebar, scheme[scm][ColBorder].pixel);
+	drw_setscheme(drw, scheme[scm]);
+
+	/* the buttons are right aligned, the title gets the rest of the strip */
+	tw = titlebtnsw();
+	w = c->w - tw;
+	if (w < lrpad) { /* too narrow for both: drop the buttons */
+		tw = 0;
+		w = c->w;
+	}
+	lpad = lrpad / 2;
+	if (centertitle && (int)TEXTW(c->name) < w)
+		lpad = (w - (int)TEXTW(c->name) + lrpad) / 2;
+	drw_text(drw, 0, 0, w, th, lpad, c->name, 0);
+	for (i = 0, x = w; tw && i < LENGTH(titlebarbtns); i++) {
+		bw = TEXTW(titlebarbtns[i].label);
+		drw_text(drw, x, 0, bw, th, lrpad / 2, titlebarbtns[i].label,
+			c->hoverbtn == (int)i);
+		x += bw;
+	}
+	drw_map(drw, c->titlebar, 0, 0, c->w, th);
+}
+
+void
+drawtitlebars(void)
+{
+	Client *c;
+	Monitor *m;
+
+	for (m = mons; m; m = m->next)
+		for (c = m->clients; c; c = c->next)
+			if (ISVISIBLE(c))
+				drawtitlebar(c);
+}
+
 void
 enternotify(XEvent *e)
 {
_AT_@ -765,7 +965,8 @@ enternotify(XEvent *e)
 
 	if ((ev->mode != NotifyNormal || ev->detail == NotifyInferior) && ev->window != root)
 		return;
-	c = wintoclient(ev->window);
+	if (!(c = wintoclient(ev->window)))
+		c = titlebartoclient(ev->window);
 	m = c ? c->mon : wintomon(ev->window);
 	if (m != selmon) {
 		unfocus(selmon->sel, 1);
_AT_@ -778,10 +979,15 @@ enternotify(XEvent *e)
 void
 expose(XEvent *e)
 {
+	Client *c;
 	Monitor *m;
 	XExposeEvent *ev = &e->xexpose;
 
-	if (ev->count == 0 && (m = wintomon(ev->window)))
+	if (ev->count)
+		return;
+	if ((c = titlebartoclient(ev->window)))
+		drawtitlebar(c);
+	else if ((m = wintomon(ev->window)))
 		drawbar(m);
 }
 
_AT_@ -808,6 +1014,7 @@ focus(Client *c)
 	}
 	selmon->sel = c;
 	drawbars();
+	drawtitlebars();
 }
 
 /* there are some broken focus acquiring clients needing extra handling */
_AT_@ -977,6 +1184,20 @@ grabkeys(void)
 	}
 }
 
+/* minimise: the client stays managed but is skipped by ISVISIBLE until
+ * showhidden() brings it back */
+void
+hide(const Arg *arg)
+{
+	Client *c = selmon->sel;
+
+	if (!c || c->isfullscreen)
+		return;
+	c->ishidden = 1;
+	focus(NULL);
+	arrange(c->mon);
+}
+
 void
 incnmaster(const Arg *arg)
 {
_AT_@ -1028,6 +1249,18 @@ killclient(const Arg *arg)
 	}
 }
 
+void
+leavenotify(XEvent *e)
+{
+	Client *c;
+	XCrossingEvent *ev = &e->xcrossing;
+
+	if ((c = titlebartoclient(ev->window)) && c->hoverbtn != -1) {
+		c->hoverbtn = -1;
+		drawtitlebar(c);
+	}
+}
+
 void
 manage(Window w, XWindowAttributes *wa)
 {
_AT_@ -1060,6 +1293,9 @@ manage(Window w, XWindowAttributes *wa)
 	c->x = MAX(c->x, c->mon->wx);
 	c->y = MAX(c->y, c->mon->wy);
 	c->bw = borderpx;
+	createtitlebar(c);
+	/* wa->height is the size the client asked for, the titlebar comes on top */
+	c->h = c->oldh = c->h + titleh(c);
 
 	wc.border_width = c->bw;
 	XConfigureWindow(dpy, w, CWBorderWidth, &wc);
_AT_@ -1074,6 +1310,7 @@ manage(Window w, XWindowAttributes *wa)
 		c->isfloating = c->oldstate = trans != None || c->isfixed;
 	if (c->isfloating)
 		XRaiseWindow(dpy, c->win);
+	stacktitlebar(c);
 	attach(c);
 	attachstack(c);
 	XChangeProperty(dpy, root, netatom[NetClientList], XA_WINDOW, 32, PropModeAppend,
_AT_@ -1129,11 +1366,19 @@ void
 motionnotify(XEvent *e)
 {
 	static Monitor *mon = NULL;
+	Client *c;
 	Monitor *m;
 	XMotionEvent *ev = &e->xmotion;
+	int btn;
 
-	if (ev->window != root)
+	if (ev->window != root) {
+		if ((c = titlebartoclient(ev->window))
+		&& (btn = titlebtnat(c, ev->x)) != c->hoverbtn) {
+			c->hoverbtn = btn;
+			drawtitlebar(c);
+		}
 		return;
+	}
 	if ((m = recttomon(ev->x_root, ev->y_root, 1, 1)) != mon && mon) {
 		unfocus(selmon->sel, 1);
 		selmon = m;
_AT_@ -1249,6 +1494,7 @@ propertynotify(XEvent *e)
 			updatetitle(c);
 			if (c == c->mon->sel)
 				drawbar(c->mon);
+			drawtitlebar(c);
 		}
 		if (ev->atom == netatom[NetWMWindowType])
 			updatewindowtype(c);
_AT_@ -1285,15 +1531,35 @@ resize(Client *c, int x, int y, int w, int h, int interact)
 void
 resizeclient(Client *c, int x, int y, int w, int h)
 {
-	XWindowChanges wc;
+	XWindowChanges wc, twc;
+	int dh;
 
 	c->oldx = c->x; c->x = wc.x = x;
 	c->oldy = c->y; c->y = wc.y = y;
 	c->oldw = c->w; c->w = wc.width = w;
 	c->oldh = c->h; c->h = wc.height = h;
 	wc.border_width = c->bw;
+	/* x, y, w and h describe the whole frame: the titlebar takes its top
+	 * strip and the managed window gets everything below, so a decorated
+	 * client covers exactly the same area an undecorated one would */
+	if ((dh = titleh(c))) {
+		wc.y += dh;
+		wc.height = MAX(h - dh, 1);
+		twc.x = x;
+		twc.y = y;
+		twc.width = w;
+		twc.height = th;
+		twc.border_width = c->bw;
+		XConfigureWindow(dpy, c->titlebar,
+			CWX|CWY|CWWidth|CWHeight|CWBorderWidth, &twc);
+		if (ISVISIBLE(c))
+			XMapWindow(dpy, c->titlebar);
+	} else if (c->titlebar)
+		XUnmapWindow(dpy, c->titlebar);
 	XConfigureWindow(dpy, c->win, CWX|CWY|CWWidth|CWHeight|CWBorderWidth, &wc);
 	configure(c);
+	stacktitlebar(c);
+	drawtitlebar(c);
 	XSync(dpy, False);
 }
 
_AT_@ -1316,7 +1582,8 @@ resizemouse(const Arg *arg)
 	if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync,
 		None, cursor[CurResize]->cursor, CurrentTime) != GrabSuccess)
 		return;
-	XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1);
+	XWarpPointer(dpy, None, c->win, 0, 0, 0, 0,
+		c->w + c->bw - 1, c->h - titleh(c) + c->bw - 1);
 	do {
 		XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev);
 		switch(ev.type) {
_AT_@ -1344,7 +1611,8 @@ resizemouse(const Arg *arg)
 			break;
 		}
 	} while (ev.type != ButtonRelease);
-	XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1);
+	XWarpPointer(dpy, None, c->win, 0, 0, 0, 0,
+		c->w + c->bw - 1, c->h - titleh(c) + c->bw - 1);
 	XUngrabPointer(dpy, CurrentTime);
 	while (XCheckMaskEvent(dpy, EnterWindowMask, &ev));
 	if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) {
_AT_@ -1375,6 +1643,9 @@ restack(Monitor *m)
 				wc.sibling = c->win;
 			}
 	}
+	for (c = m->stack; c; c = c->snext)
+		if (ISVISIBLE(c))
+			stacktitlebar(c);
 	XSync(dpy, False);
 	while (XCheckMaskEvent(dpy, EnterWindowMask, &ev));
 }
_AT_@ -1563,6 +1834,8 @@ setup(void)
 		die("no fonts could be loaded.");
 	lrpad = drw->fonts->h;
 	bh = drw->fonts->h + 2;
+	th = titlebarheight > 0 ? titlebarheight : bh;
+	th = MAX(th, (int)drw->fonts->h); /* the title has to fit */
 	updategeom();
 	/* init atoms */
 	utf8string = XInternAtom(dpy, "UTF8_STRING", False);
_AT_@ -1631,19 +1904,38 @@ showhide(Client *c)
 {
 	if (!c)
 		return;
-	if (ISVISIBLE(c)) {
+	if (ISVISIBLE(c) && !(((getstate(c->win) == IconicState))) ) {
 		/* show clients top down */
-		XMoveWindow(dpy, c->win, c->x, c->y);
+		XMoveWindow(dpy, c->win, c->x, c->y + titleh(c));
+		if (titleh(c)) {
+			XMoveWindow(dpy, c->titlebar, c->x, c->y);
+			XMapWindow(dpy, c->titlebar);
+			drawtitlebar(c);
+		}
 		if ((!c->mon->lt[c->mon->sellt]->arrange || c->isfloating) && !c->isfullscreen)
 			resize(c, c->x, c->y, c->w, c->h, 0);
 		showhide(c->snext);
 	} else {
 		/* hide clients bottom up */
 		showhide(c->snext);
+		if (c->titlebar)
+			XUnmapWindow(dpy, c->titlebar);
 		XMoveWindow(dpy, c->win, WIDTH(c) * -2, c->y);
 	}
 }
 
+/* bring back every client minimised with hide() on this monitor */
+void
+showhidden(const Arg *arg)
+{
+	Client *c;
+
+	for (c = selmon->clients; c; c = c->next)
+		showhide(c); c->ishidden = 0;
+	focus(NULL);
+	arrange(selmon);
+}
+
 void
 spawn(const Arg *arg)
 {
_AT_@ -1666,6 +1958,36 @@ spawn(const Arg *arg)
 	}
 }
 
+/* the titlebar is an ordinary sibling of the window it decorates, so it has
+ * to be restacked right on top of it whenever the stacking order changes */
+void
+stacktitlebar(Client *c)
+{
+	XWindowChanges wc = { .sibling = c->win, .stack_mode = Above };
+
+	if (c->titlebar)
+		XConfigureWindow(dpy, c->titlebar, CWSibling|CWStackMode, &wc);
+}
+
+/* exchange the position of two clients in the client list */
+void
+swapclients(Monitor *m, Client *a, Client *b)
+{
+	Client **pa, **pb, *t;
+
+	if (!a || !b || a == b)
+		return;
+	for (pa = &m->clients; *pa && *pa != a; pa = &(*pa)->next);
+	for (pb = &m->clients; *pb && *pb != b; pb = &(*pb)->next);
+	if (!*pa || !*pb)
+		return;
+	*pa = b;
+	*pb = a;
+	t = a->next;
+	a->next = b->next;
+	b->next = t;
+}
+
 void
 tag(const Arg *arg)
 {
_AT_@ -1712,6 +2034,56 @@ tile(Monitor *m)
 		}
 }
 
+Client *
+titlebartoclient(Window w)
+{
+	Client *c;
+	Monitor *m;
+
+	if (!w)
+		return NULL;
+	for (m = mons; m; m = m->next)
+		for (c = m->clients; c; c = c->next)
+			if (c->titlebar == w)
+				return c;
+	return NULL;
+}
+
+/* index of the titlebar button at x (relative to the titlebar), -1 for none */
+int
+titlebtnat(Client *c, int x)
+{
+	unsigned int i;
+	int bx = c->w - titlebtnsw();
+
+	if (bx < lrpad || x < bx)
+		return -1;
+	for (i = 0; i < LENGTH(titlebarbtns); i++) {
+		bx += TEXTW(titlebarbtns[i].label);
+		if (x < bx)
+			return i;
+	}
+	return -1;
+}
+
+int
+titlebtnsw(void)
+{
+	unsigned int i;
+	int w = 0;
+
+	for (i = 0; i < LENGTH(titlebarbtns); i++)
+		w += TEXTW(titlebarbtns[i].label);
+	return w;
+}
+
+/* vertical space c's decoration takes inside its frame, 0 when undecorated */
+int
+titleh(Client *c)
+{
+	return showtitlebar && c->titlebar && !c->isfullscreen ? th + 2 * c->bw : 0;
+}
+
 void
 togglebar(const Arg *arg)
 {
_AT_@ -1783,6 +2155,10 @@ unmanage(Client *c, int destroyed)
 
 	detach(c);
 	detachstack(c);
+	if (c->titlebar) {
+		XUnmapWindow(dpy, c->titlebar);
+		XDestroyWindow(dpy, c->titlebar);
+	}
 	if (!destroyed) {
 		wc.border_width = c->oldbw;
 		XGrabServer(dpy); /* avoid race conditions */
_AT_@ -2087,7 +2463,7 @@ wintomon(Window w)
 	for (m = mons; m; m = m->next)
 		if (w == m->barwin)
 			return m;
-	if ((c = wintoclient(w)))
+	if ((c = wintoclient(w)) || (c = titlebartoclient(w)))
 		return c->mon;
 	return selmon;
 }
-- 
2.53.0
Received on Sun Aug 23 2026 - 07:43:39 CEST

This archive was generated by hypermail 2.3.0 : Sun Aug 23 2026 - 15:24:35 CEST