diff options
Diffstat (limited to 'window-switcher')
| -rw-r--r-- | window-switcher/Expose.js | 79 | ||||
| -rw-r--r-- | window-switcher/test_expose.js | 59 |
2 files changed, 138 insertions, 0 deletions
diff --git a/window-switcher/Expose.js b/window-switcher/Expose.js new file mode 100644 index 0000000..2b14e4e --- /dev/null +++ b/window-switcher/Expose.js @@ -0,0 +1,79 @@ +.pragma library + +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// The expose scatter, kept pure so it runs under node (test_expose.js). +// Input is [{w, h}] in real pixels, most recently used first; output is +// [{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. + +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 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. + 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++) { + const a = r[i], b = r[j]; + const ox = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) + GAP; + const oy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) + GAP; + if (ox <= 0 || oy <= 0) continue; + moved = true; + // Push along whichever axis costs less screen, measured against + // the screen's own width and height: compared in raw pixels, wide + // windows always separate vertically and stack into a column. + if (ox / W < oy / H) { + const s = (b.x + b.w / 2) >= (a.x + a.w / 2) ? 1 : -1; + a.x -= s * ox / 2; b.x += s * ox / 2; + } else { + const s = (b.y + b.h / 2) >= (a.y + a.h / 2) ? 1 : -1; + a.y -= s * oy / 2; b.y += s * oy / 2; + } + } + if (!moved) break; + } + + 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); + x1 = Math.max(x1, o.x + o.w); y1 = Math.max(y1, o.y + o.h); + } + const k = Math.min(FILL * W / (x1 - x0), FILL * H / (y1 - y0), MAX_SCALE); + const dx = (W - (x1 - x0) * k) / 2, dy = (H - (y1 - y0) * k) / 2; + return r.map(o => ({ x: dx + (o.x - x0) * k, y: dy + (o.y - y0) * k, + w: o.w * k, h: o.h * k })); +} diff --git a/window-switcher/test_expose.js b/window-switcher/test_expose.js new file mode 100644 index 0000000..d394253 --- /dev/null +++ b/window-switcher/test_expose.js @@ -0,0 +1,59 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// Run: node window-switcher/test_expose.js +// Expose.js is a QML library, so its .pragma line is stripped before node +// evaluates it, as test_perf.js does for Perf.js. + +const fs = require("fs"); +const assert = require("assert"); + +const src = fs.readFileSync(__dirname + "/Expose.js", "utf8") + .replace(/^\.pragma library$/m, ""); +const { layout } = new Function(src + "\nreturn { layout };")(); + +const W = 2560, H = 1080; +const wide = { w: 2556, h: 1076 }, tall = { w: 1076, h: 1916 }; + +function apart(a, b) { + return a.x + a.w <= b.x || b.x + b.w <= a.x || a.y + a.h <= b.y || b.y + b.h <= a.y; +} +function check(out) { + for (const o of out) { + assert.ok(o.x >= 0 && o.y >= 0 && o.x + o.w <= W + 1e-6 && o.y + o.h <= H + 1e-6, "inside the screen"); + } + for (let i = 0; i < out.length; i++) + for (let j = i + 1; j < out.length; j++) + assert.ok(apart(out[i], out[j]), `windows ${i} and ${j} overlap`); +} + +assert.deepStrictEqual(layout([], W, H), []); + +// Seven identical full-screen windows, the normal case here: no overlap. +const seven = Array(7).fill(wide); +check(layout(seven, W, H)); + +// A busy mixed desk still separates within the pass limit. +check(layout(Array.from({ length: 14 }, (_, i) => i % 3 ? wide : tall), W, H)); + +// Deterministic: the same input gives the same scatter. +assert.deepStrictEqual(layout(seven, W, H), layout(seven, W, H)); + +// A lone window is capped at half size. +const one = layout([wide], W, H)[0]; +assert.ok(Math.abs(one.w - wide.w * 0.5) < 1e-6); + +// One scale for all: sizes stay relative. +const [a, b] = layout([wide, tall], W, H); +assert.ok(Math.abs(a.w / b.w - wide.w / tall.w) < 1e-9); +assert.ok(Math.abs(a.h / b.h - wide.h / tall.h) < 1e-9); + +console.log("test_expose: ok"); |
