#include "wm.h" /* ── WM_STATE ICCCM helper ───────────────────────────────────────────── */ void client_set_wm_state(WM *wm, Client *c, long state) { long data[2] = { state, None }; XChangeProperty(wm->dpy, c->win, wm->wm_state, wm->wm_state, 32, PropModeReplace, (unsigned char *)data, 2); if (state == WM_STATE_ICONIC) { XChangeProperty(wm->dpy, c->win, wm->net_wm_state, XA_ATOM, 32, PropModeReplace, (unsigned char *)&wm->net_wm_state_hidden, 1); } else { XDeleteProperty(wm->dpy, c->win, wm->net_wm_state); } } static void update_net_client_list(WM *wm) { Atom net_client_list = XInternAtom(wm->dpy, "_NET_CLIENT_LIST", False); int count = 0; for (Client *c = wm->clients; c; c = c->next) count++; Window *wins = calloc(count, sizeof(Window)); int i = 0; for (Client *c = wm->clients; c; c = c->next) wins[i++] = c->win; XChangeProperty(wm->dpy, wm->root, net_client_list, XA_WINDOW, 32, PropModeReplace, (unsigned char *)wins, count); free(wins); } /* ── add / remove ────────────────────────────────────────────────────── */ /* smart placement: cascade from top-left, offset each window */ static void smart_place(WM *wm, Client *c) { static int cascade_x = 40, cascade_y = 40; int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); /* reset cascade if it would go off-screen */ if (cascade_x + (int)c->w > sw - 40 || cascade_y + (int)c->h > sh - 40) { cascade_x = 40; cascade_y = 40; } c->x = cascade_x; c->y = cascade_y; cascade_x += 30; cascade_y += 30; } Client *client_add(WM *wm, Window win) { XWindowAttributes wa; if (!XGetWindowAttributes(wm->dpy, win, &wa)) return NULL; if (wa.override_redirect) return NULL; if (client_find_by_win(wm, win)) return NULL; Client *c = calloc(1, sizeof(Client)); c->win = win; c->w = wa.width; c->h = wa.height; c->desktop = wm->cur_desktop; c->min_w = MIN_WIN_W; c->min_h = MIN_WIN_H; /* use application-requested position if non-zero, otherwise smart place */ if (wa.x == 0 && wa.y == 0) { smart_place(wm, c); } else { c->x = wa.x; c->y = wa.y; } /* never land behind deskbar */ int sw = DisplayWidth(wm->dpy, wm->screen); if (c->x + (int)c->w > sw - DESKBAR_W && c->y < 200) c->y = 40; /* fetch title */ char *name = NULL; Atom utf8 = XInternAtom(wm->dpy, "UTF8_STRING", False); Atom net_wm = XInternAtom(wm->dpy, "_NET_WM_NAME", False); { Atom type; int fmt; unsigned long n, after; unsigned char *data = NULL; if (XGetWindowProperty(wm->dpy, win, net_wm, 0, 256, False, utf8, &type, &fmt, &n, &after, &data) == Success && data && n > 0) { snprintf(c->title, 255, "%.*s", (int)n, data); XFree(data); } else { if (data) XFree(data); if (XFetchName(wm->dpy, win, &name) && name) { strncpy(c->title, name, 255); XFree(name); } else { strncpy(c->title, "Untitled", 255); } } } /* ICCCM WM_NORMAL_HINTS — size constraints and resize increments */ { XSizeHints hints; long supplied = 0; if (XGetWMNormalHints(wm->dpy, win, &hints, &supplied)) { if (hints.flags & PMinSize) { if (hints.min_width > c->min_w) c->min_w = hints.min_width; if (hints.min_height > c->min_h) c->min_h = hints.min_height; } if (hints.flags & PResizeInc) { c->inc_w = hints.width_inc; c->inc_h = hints.height_inc; } if (hints.flags & PBaseSize) { c->base_w = hints.base_width; c->base_h = hints.base_height; } else if (hints.flags & PMinSize) { c->base_w = hints.min_width; c->base_h = hints.min_height; } } } /* advertise which desktop this window lives on */ { long desk = wm->cur_desktop; XChangeProperty(wm->dpy, win, wm->net_wm_desktop, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desk, 1); } int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; XSetWindowAttributes swa; swa.background_pixel = color_alloc(wm, COLOR_TITLE_INACTIVE); swa.event_mask = SubstructureRedirectMask | SubstructureNotifyMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ExposureMask | EnterWindowMask; swa.override_redirect = False; swa.cursor = wm->cur_normal; c->frame = XCreateWindow( wm->dpy, wm->root, c->x, c->y, fw, fh, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask | CWOverrideRedirect | CWCursor, &swa ); XReparentWindow(wm->dpy, win, c->frame, BORDER_W, TITLE_HEIGHT); XSelectInput(wm->dpy, win, PropertyChangeMask | FocusChangeMask); c->next = wm->clients; wm->clients = c; XMapWindow(wm->dpy, c->frame); XMapWindow(wm->dpy, win); client_set_wm_state(wm, c, WM_STATE_NORMAL); update_net_client_list(wm); client_focus(wm, c); return c; } void client_remove(WM *wm, Window win) { Client *prev = NULL, *c = wm->clients; while (c) { if (c->win == win || c->frame == win) { if (prev) prev->next = c->next; else wm->clients = c->next; if (wm->focused == c) { wm->focused = wm->clients; if (wm->focused) client_focus(wm, wm->focused); else wm->focused = NULL; } XDestroyWindow(wm->dpy, c->frame); free(c); update_net_client_list(wm); return; } prev = c; c = c->next; } } /* ── lookup ──────────────────────────────────────────────────────────── */ Client *client_find_by_frame(WM *wm, Window frame) { for (Client *c = wm->clients; c; c = c->next) if (c->frame == frame) return c; return NULL; } Client *client_find_by_win(WM *wm, Window win) { for (Client *c = wm->clients; c; c = c->next) if (c->win == win) return c; return NULL; } /* ── focus / raise ───────────────────────────────────────────────────── */ void client_focus(WM *wm, Client *c) { if (!c || c->minimized) return; if (wm->focused && wm->focused != c) { wm->focused->focused = 0; frame_draw(wm, wm->focused); } wm->focused = c; c->focused = 1; XSetInputFocus(wm->dpy, c->win, RevertToPointerRoot, CurrentTime); XRaiseWindow(wm->dpy, c->frame); frame_draw(wm, c); /* advertise active window */ Atom net_active = XInternAtom(wm->dpy, "_NET_ACTIVE_WINDOW", False); XChangeProperty(wm->dpy, wm->root, net_active, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&c->win, 1); } void client_raise(WM *wm, Client *c) { XRaiseWindow(wm->dpy, c->frame); } /* ── minimize / restore ──────────────────────────────────────────────── */ void client_minimize(WM *wm, Client *c) { if (c->minimized) return; if (c->maximized) client_unmaximize(wm, c); c->minimized = 1; c->focused = 0; XUnmapWindow(wm->dpy, c->frame); client_set_wm_state(wm, c, WM_STATE_ICONIC); if (wm->focused == c) { wm->focused = NULL; for (Client *n = wm->clients; n; n = n->next) { if (!n->minimized) { client_focus(wm, n); break; } } } } void client_restore(WM *wm, Client *c) { if (!c->minimized) { client_focus(wm, c); return; } c->minimized = 0; XMapWindow(wm->dpy, c->frame); client_set_wm_state(wm, c, WM_STATE_NORMAL); client_focus(wm, c); } /* ── maximize / unmaximize ───────────────────────────────────────────── */ void client_maximize(WM *wm, Client *c) { if (c->maximized || c->minimized) return; /* save current geometry */ c->saved_x = c->x; c->saved_y = c->y; c->saved_w = c->w; c->saved_h = c->h; c->maximized = 1; int sw = DisplayWidth(wm->dpy, wm->screen); int sh = DisplayHeight(wm->dpy, wm->screen); /* leave room for deskbar on the right */ int deskbar_w = DESKBAR_W; int new_w = sw - deskbar_w - BORDER_W * 2; int new_h = sh - TITLE_HEIGHT - BORDER_W; c->x = 0; c->y = 0; c->w = (unsigned)new_w; c->h = (unsigned)new_h; int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); XResizeWindow(wm->dpy, c->win, c->w, c->h); /* advertise maximized state via EWMH */ Atom states[2] = { wm->net_wm_state_maximized_vert, wm->net_wm_state_maximized_horz }; XChangeProperty(wm->dpy, c->win, wm->net_wm_state, XA_ATOM, 32, PropModeReplace, (unsigned char *)states, 2); client_focus(wm, c); frame_draw(wm, c); } void client_unmaximize(WM *wm, Client *c) { if (!c->maximized) return; c->maximized = 0; c->x = c->saved_x; c->y = c->saved_y; c->w = c->saved_w; c->h = c->saved_h; int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); XResizeWindow(wm->dpy, c->win, c->w, c->h); XDeleteProperty(wm->dpy, c->win, wm->net_wm_state); client_focus(wm, c); frame_draw(wm, c); } void client_toggle_maximize(WM *wm, Client *c) { if (c->maximized) client_unmaximize(wm, c); else client_maximize(wm, c); } /* ── edge snap ───────────────────────────────────────────────────────── */ 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 = DESKBAR_W; int half_w = (sw - deskbar_w) / 2; c->x = 0; c->y = 0; c->w = (unsigned)(half_w - BORDER_W * 2); c->h = (unsigned)(sh - TITLE_HEIGHT - BORDER_W); int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); XResizeWindow(wm->dpy, c->win, c->w, c->h); frame_draw(wm, c); } void client_move_to_desktop(WM *wm, Client *c, int new_desk) { if (new_desk < 0 || new_desk >= 4 || c->desktop == new_desk) return; /* hide frame if it's on the current (now-active) desktop */ if (c->desktop == wm->cur_desktop && !c->minimized) XUnmapWindow(wm->dpy, c->frame); c->desktop = new_desk; /* update _NET_WM_DESKTOP */ long desk = new_desk; XChangeProperty(wm->dpy, c->win, wm->net_wm_desktop, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&desk, 1); /* if we moved the focused window away, re-focus another */ if (wm->focused == c) { wm->focused->focused = 0; frame_draw(wm, wm->focused); wm->focused = NULL; for (Client *n = wm->clients; n; n = n->next) { if (!n->minimized && n->desktop == wm->cur_desktop) { client_focus(wm, n); break; } } } } 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 = DESKBAR_W; int half_w = (sw - deskbar_w) / 2; c->x = half_w; c->y = 0; c->w = (unsigned)(sw - deskbar_w - half_w - BORDER_W * 2); c->h = (unsigned)(sh - TITLE_HEIGHT - BORDER_W); int fw = (int)c->w + BORDER_W * 2; int fh = (int)c->h + TITLE_HEIGHT + BORDER_W; XMoveResizeWindow(wm->dpy, c->frame, c->x, c->y, fw, fh); XResizeWindow(wm->dpy, c->win, c->w, c->h); frame_draw(wm, c); }