aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-15 11:03:48 +0200
committerDanilo M. <danix@danix.xyz>2026-10-15 11:03:48 +0200
commit4cc85e59d7b3f53f4eec897f022bc4bcb643f185 (patch)
tree4118798e9650103dde14da765c8d414068a4c101
parentfcd5d1c48e5993c9b452bfe824900d9a80df78c3 (diff)
downloadquickshell-4cc85e59d7b3f53f4eec897f022bc4bcb643f185.tar.gz
quickshell-4cc85e59d7b3f53f4eec897f022bc4bcb643f185.zip
feat(window-switcher): gravity pulls the scatter toward the centre
On screen the spiral alone read as loose: separation keeps the spiral's gaps, and the previews filled 24 to 30% of the screen. Thirty rounds now pull every window 15% closer to the centre and push them apart again, which fills 35 to 55% and makes each preview up to 1.5x larger. Twenty windows lay out in about 4ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--docs/superpowers/specs/2026-10-15-expose-design.md5
-rw-r--r--window-switcher/Expose.js59
2 files changed, 42 insertions, 22 deletions
diff --git a/docs/superpowers/specs/2026-10-15-expose-design.md b/docs/superpowers/specs/2026-10-15-expose-design.md
index cc4eac9..488ab6a 100644
--- a/docs/superpowers/specs/2026-10-15-expose-design.md
+++ b/docs/superpowers/specs/2026-10-15-expose-design.md
@@ -78,7 +78,10 @@ pointer stops there.
width or height. Compared in raw pixels, wide windows always separate
vertically and stack into a column. Stops when a pass moves nothing, or
after 300 passes.
-3. **Fit.** One uniform scale fits the bounding box into 92% of W by H,
+3. **Gravity.** Added after the first look on screen, where the scatter read
+ as loose: thirty rounds each pull every rect 15% closer to the centre and
+ separate again. Fill went from 24-30% of the screen to 35-55%.
+4. **Fit.** One uniform scale fits the bounding box into 92% of W by H,
capped at 0.5 so a lone window is not a full-screen mirror, and the result
is centred. A uniform scale keeps rects apart and keeps their sizes
relative to each other.
diff --git a/window-switcher/Expose.js b/window-switcher/Expose.js
index 2b14e4e..4120907 100644
--- a/window-switcher/Expose.js
+++ b/window-switcher/Expose.js
@@ -16,35 +16,24 @@
// [{x, y, w, h}] inside a W by H screen.
//
// Windows are seeded on a golden-angle spiral, the most recent at the centre,
-// then pushed apart until nothing overlaps, then fitted with one scale, so
-// their sizes stay relative to each other. Seeding from the windows' real
-// positions was tried first and degenerates here: every window fills its
-// monitor, so they all start in one pile, and pushing a pile apart makes a
-// line that still overlapped after 300 passes and filled under 15% of the
-// screen.
+// then pulled toward the centre and pushed apart again for a number of
+// rounds, so the scatter is a cluster rather than a loose spiral, then
+// fitted with one scale, so their sizes stay relative to each other.
+// Seeding from the windows' real positions was tried first and degenerates
+// here: every window fills its monitor, so they all start in one pile, and
+// pushing a pile apart makes a line that still overlapped after 300 passes
+// and filled under 15% of the screen.
const GAP = 60; // real pixels kept between two windows
const PASSES = 300;
const FILL = 0.92; // of the screen, each way
const MAX_SCALE = 0.5; // a lone window is not a full-screen mirror
const SPREAD = 0.45; // spiral step, as a fraction of the tallest window
+const ROUNDS = 30;
+const PULL = 0.85; // centre distance kept per gravity round
const GOLDEN = 137.5 * Math.PI / 180;
-function layout(rects, W, H) {
- if (rects.length === 0) return [];
- const aspect = W / H;
- const step = SPREAD * Math.max(...rects.map(o => o.h));
-
- // The spiral is stretched by the screen's aspect, so the cluster comes
- // out the shape of the screen rather than round.
- const r = rects.map((o, k) => {
- const rad = step * Math.sqrt(k), t = k * GOLDEN;
- return { x: rad * Math.cos(t) * aspect - o.w / 2,
- y: rad * Math.sin(t) - o.h / 2, w: o.w, h: o.h };
- });
-
- // ponytail: all pairs every pass, O(n^2); fine for this desk's ~20
- // windows, a spatial grid if it ever is not.
+function separate(r, W, H) {
for (let pass = 0; pass < PASSES; pass++) {
let moved = false;
for (let i = 0; i < r.length; i++) for (let j = i + 1; j < r.length; j++) {
@@ -67,6 +56,34 @@ function layout(rects, W, H) {
if (!moved) break;
}
+}
+
+function layout(rects, W, H) {
+ if (rects.length === 0) return [];
+ const aspect = W / H;
+ const step = SPREAD * Math.max(...rects.map(o => o.h));
+
+ // The spiral is stretched by the screen's aspect, so the cluster comes
+ // out the shape of the screen rather than round.
+ const r = rects.map((o, k) => {
+ const rad = step * Math.sqrt(k), t = k * GOLDEN;
+ return { x: rad * Math.cos(t) * aspect - o.w / 2,
+ y: rad * Math.sin(t) - o.h / 2, w: o.w, h: o.h };
+ });
+
+ // Gravity: everything is pulled toward the centre, then pushed apart
+ // again, until the cluster stops shrinking. Separation alone leaves
+ // the spiral's gaps in place and the scatter reads as loose.
+ // ponytail: all pairs every pass, O(n^2); fine for this desk's ~20
+ // windows, a spatial grid if it ever is not.
+ for (let round = 0; round < ROUNDS; round++) {
+ for (const o of r) {
+ o.x = (o.x + o.w / 2) * PULL - o.w / 2;
+ o.y = (o.y + o.h / 2) * PULL - o.h / 2;
+ }
+ separate(r, W, H);
+ }
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const o of r) {
x0 = Math.min(x0, o.x); y0 = Math.min(y0, o.y);