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.

On click: yield, hard grab, or soft grab?

On the frame you press the mouse, each buffer measures the signed distance from the click to both windows’ edges and sorts the click into one of three cases:

  • Click landed inside the other window → yield (store the inactive sentinel 999).
  • Click landed inside mehard grab: remember the exact offset from my corner to the cursor.
  • Click landed on empty background → soft grab: remember the same offset, but tagged with +100 so the next stage knows to tug gently instead of locking on.

Packing three states into one float channel is the trick — anything below 50 is a hard grab, 50900 is a soft grab (minus the 100 tag), and 999 means inactive.

Buffer A and Buffer B run this negotiation with the roles swapped: Buffer A checks the other window first (so it yields in an overlap), Buffer B checks itself first (so it wins) — which is what makes B behave as the top window.

if (dOther <= 0.0)   C.ba = vec2(999.0);          // yield
else if (dMe <= 0.0) C.ba = myPos - cm;           // hard grab
else                 C.ba = (myPos - cm) + 100.0; // soft grab (+100 tag)
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, with a blend weight that’s strongest right under the cursor and falls off with distance:

float w = iMouse.z > 0. ? max(0.01, exp(-35.*pow(d,1.9))) : 0.1;
if (isSoft) w *= 0.25 * exp(-3.0 * distToMouse); // soft grab: weaker, fades with range
C.rg = mix(C.rg, target, w);

A hard grab (you clicked the window itself) locks on firmly — pixels under your cursor snap to the target, the rest follow a beat behind. A soft grab (you clicked empty space) only tugs, and the tug weakens the further the window sits from your cursor, so a distant window drifts over lazily while a near one comes quickly to hand. 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 width 0.34 wide and height 0.21 tall, a darker title bar across the top, three traffic-light buttons, and a thin border. The corner radius is radius 0.030.

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

The rounded rectangle (sdBox) → technical explainer

sdBox is the one piece of shared math, shared across the passes in common.glsl. 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;
↗ Shadertoy
1 void mainImage(out vec4 C, in vec2 XY) {
2 if (iFrame < 3) { C = vec4(-0.30, 0.05, 999.0, 999.0); return; } // spawn inactive
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
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 if (dOther <= 0.0) C.ba = vec2(999.0); // other window owns the overlap — yield
19 else if (dMe <= 0.0) C.ba = myPos - cm; // clicked directly on me — hard grab
20 else C.ba = (myPos - cm) + 100.0; // clicked empty space — soft grab (+100 tag)
21
22 }
23
24 C.rg = mix(C.rg, texture(iChannel0, uv, 1.5).rg, 0.15); // gently diffuse the stored field
25
26 bool isHard = (C.ba.x < 50.0);
27 bool isSoft = (C.ba.x > 50.0 && C.ba.x < 900.0);
28 bool isActive = isHard || isSoft;
29 vec2 offset = isSoft ? (C.ba - 100.0) : C.ba; // undo the +100 tag
30 vec2 target = isActive ? (m + offset) : C.rg;
31
32
33 float d = length(uv - mUV);
34 float w = iMouse.z > 0. ? max(0.01, exp(-35.*pow(d,1.9))) : 0.1;
35 if (isSoft) { // soft grab: weaker pull, fades with distance
36 float distToMouse = length(C.rg - m);
37 w *= 0.25 * exp(-3.0 * distToMouse);
38 }
39 C.rg = mix(C.rg, target, w); // ease toward the target
40
41 }
42