fragsplainer / Draggable Windows
A Shader, Explained

Draggable Windows

A working window manager — written entirely in pixels.

Most shaders are pure math: the same picture every frame, with no memory of the last one. This one is different. It remembers where you left each window — which is the whole reason you can pick them up and drag them. Go ahead, grab a title bar in the panel on the left.


The trick is multipass feedback: a couple of off-screen buffers that each read their own previous frame. Nothing is stored the way a normal program stores a variable — the state simply copies itself forward, frame after frame, living inside textures.

peek at the memory

Pass · Buffers A & B — the memory

There are two buffers, one per window — near-identical twins. Each one keeps its window’s position in the red & green channels, and, while you’re dragging, a grab offset in blue & alpha. Every frame the buffer eases its stored position toward a target.

Who ends up on top?

The hard part of any window manager is deciding which window has focus when they overlap. Each buffer measures the signed distance from the click to both windows’ edges, then negotiates:

  • If the click landed inside the other window, yield to it.
  • Otherwise, if it landed inside me, I take focus.
  • If it’s outside both, whichever edge is nearer wins.

Buffer A and Buffer B run this same negotiation with the roles swapped — Buffer B is written to win an exact tie, so it behaves as the top window.

if (dOther <= 0.0)   isFocus = false;
else if (dMe <= 0.0) isFocus = true;
else                 isFocus = (dMe <= dOther);
Why dragging feels springy

The window doesn’t snap to the cursor — it chases it. Each frame the stored position is blended a little way toward the target, and the blend weight falls off with distance from the mouse:

float w = iMouse.z > 0. ? max(0.22, exp(-35.*pow(d,1.9))) : 0.1;
C.rg = mix(C.rg, target, w);

Pixels right under your cursor snap straight to the target; everything else follows a beat behind. The window arrives with a soft, weighty lag rather than a rigid lock — and it’s all just a mix() per pixel.

Pass · Image — the compositor

The final pass is wonderfully dumb. It knows nothing about the mouse, focus, or dragging. It just reads the two positions out of the buffers and stamps a window at each — back window first, then the front one over it.

Anatomy of a window

One window() function draws the whole chrome from distance fields: a rounded body 0.40 wide and 0.25 tall, a darker title bar across the top, three traffic-light buttons, and a thin border. The corner radius is 0.03.

Each part is a soft smoothstep over a distance, so everything stays crisp at any resolution.

The rounded rectangle (sdBox)

sdBox is the one piece of shared math, living in the Common tab. It returns the signed distance to a rounded box: negative inside, zero on the edge, positive outside. Both the buffers (for hit-testing) and the Image pass (for drawing) lean on it.

vec2 q = abs(p) - s + r;
return min(max(q.x,q.y),0.) + length(max(q,0.)) - r;
read-only · live values
1 void mainImage(out vec4 C, in vec2 XY) {
2 if (iFrame < 3) { C = vec4(-0.30, 0.05, 999.0, 999.0); return; }
3
4 vec2 uv = XY / R;
5 C = texture(iChannel0, uv); // read my own previous frame
6
7 vec2 m = (iMouse.xy - R/2.) / R.y; // cursor, now
8 vec2 cm = (abs(iMouse.zw) - R/2.) / R.y; // where the click began
9 vec2 mUV = iMouse.xy / R;
10
11 if (iMouse.w > 0.) { // the frame of a fresh click
12 -check
13 vec2 myPos = texture(iChannel0, mUV).rg;
14 vec2 otherPos = texture(iChannel1, mUV).rg;
15 float dMe = sdBox(cm - myPos, vec2(u_winW, u_winH), u_radius);
16 float dOther = sdBox(cm - otherPos, vec2(u_winW, u_winH), u_radius);
17
18 bool isFocus = false;
19 if (dOther <= 0.0) isFocus = false; // bottom owns the overlap
20 else if (dMe <= 0.0) isFocus = true;
21 else isFocus = (dMe <= dOther);
22
23
24 C.ba = isFocus ? (myPos - cm) : vec2(999.0); // remember grab offset
25 }
26
27 C.rg = mix(C.rg, texture(iChannel0, uv, 2.5).rg, 0.2);
28
29 bool isActive = (C.ba.x < 900.0);
30 vec2 target = isActive ? (m + C.ba) : C.rg;
31
32 -force
33 float d = length(uv - mUV);
34 float w = iMouse.z > 0. ? max(0.22, exp(-35.*pow(d,1.9))) : 0.1;
35 C.rg = mix(C.rg, target, w); // ease toward the target
36
37 }
38