aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/specs/2026-10-15-expose-design.md132
1 files changed, 132 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-10-15-expose-design.md b/docs/superpowers/specs/2026-10-15-expose-design.md
new file mode 100644
index 0000000..c86bd7c
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-15-expose-design.md
@@ -0,0 +1,132 @@
+# Expose design
+
+Sub-project 5 of the roadmap in `2026-10-07-island-core-design.md`: ALT+TAB
+becomes a macOS Mission Control style expose. The uniform 16:10 grid of
+`window-switcher/` is replaced by a scatter of live previews that keep their
+real shape and relative size, and a hot corner opens it with the mouse.
+
+It stays in `window-switcher/`, a separate shell: nothing in it needs to talk
+to the island or the drawer. Modesty is GPLv3 and this repo GPLv2-only; no
+code is taken from it.
+
+## What it shows
+
+Every window not on a `special:` workspace, from both monitors, on DP-1. The
+filter and the dispatches in `Windows.qml` are unchanged. There is no
+workspace strip: every window is already on screen, so the strip would only
+add a second job.
+
+## Opening and closing
+
+| | |
+| --- | --- |
+| ALT+TAB | open, or close if already open (unchanged IPC `toggle`) |
+| top-right corner of DP-1 | open after the glow completes; ignored while open |
+| click a preview | focus that window and dismiss |
+| `x` on a hovered preview | close that window, overlay stays open |
+| Escape, or a click on the backdrop | dismiss, focus unchanged |
+
+Enter, arrows and Tab go, with the selection index. This is a pointer
+feature; the keyboard keeps only the way in and Escape.
+
+Focus keeps the existing remember-close-then-focus-after-60ms path, because a
+focus dispatched under the overlay's exclusive keyboard grab is ignored.
+
+## Hot corner
+
+`HotCorner.qml`: a `PanelWindow` on DP-1, anchored top and right, 48x48,
+`WlrLayer.Overlay`, `ExclusionMode.Ignore`, no keyboard focus. Its `mask` is a
+`Region` on a 2x2 patch in the very corner, so it takes pointer input there
+and nowhere else; the rest of the 48x48 is drawn but click-through. A larger
+input area would swallow clicks meant for windows underneath, which is why
+the glow cannot start before the pointer reaches the corner.
+
+The pointer entering the patch starts a quarter-circle accent glow that grows
+out of the corner over 250ms. When it completes, the switcher's `show()` is
+called. Leaving the patch first reverses the glow and nothing opens. The
+dwell is what keeps a pointer passing through on its way somewhere from
+triggering it.
+
+The corner does nothing while gaming. It reads `status.gaming` from
+`$XDG_RUNTIME_DIR` with one watched `FileView` (a missing file is off, as in
+the status registry). `shared/Status.qml` is deliberately not linked: it is
+not passive, and a second instance would run its own game poll and react to
+presentation a second time, pausing breaktimer twice and rewriting dnd and
+nolock.
+
+DP-1 is the right-hand monitor and its top-right is a true corner, so the
+pointer stops there.
+
+## Layout
+
+`Expose.js`, pure JavaScript with no QML, like `Perf.js`:
+
+ layout(rects, W, H) -> [{x, y, w, h}] // rects: [{x, y, w, h}] in real pixels
+
+1. **Seed.** Each rect is the window's real `at` and `size` from
+ `lastIpcObject`, in global coordinates, so DP-3 windows start left of DP-1
+ ones. Where windows sit on their desktop otherwise carries no meaning.
+2. **Separate.** Repeated passes over all pairs: two rects overlapping, with
+ a 60px real gap counted as overlap, are each pushed by half the overlap
+ along the axis of least overlap. Coincident centres, the normal case here
+ because every window fills its monitor, have no direction to push along,
+ so each rect of such a pair moves along its own direction, its index times
+ the golden angle (137.5°). That is deterministic and spreads a pile in
+ every direction rather than along a line. 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,
+ 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.
+
+The input order is most recently used first, as `Windows.windows` already
+returns it, so the most recent window keeps the lowest index.
+
+A window whose `lastIpcObject` has no `at` or `size` yet gets 1280x800 at the
+origin; the layout is a binding and repaints when the data arrives. A window
+appearing or closing while the overlay is open recomputes the layout, and the
+previews move.
+
+Ceilings, documented with `ponytail:` comments: the pairwise loop is O(n²)
+per pass, fine for the twenty or so windows this desk has; previews fade in
+rather than flying from their real positions.
+
+## Preview
+
+`WindowCard.qml` is sized from outside to its layout rect instead of a fixed
+16:10 box, and the preview fills it. The app icon and the workspace badge stay
+always visible on their scrims. The title and the `x` show on hover only, and
+the hovered preview gets the accent border. The three text lines under the
+card go. The empty-desk message stays.
+
+## Files
+
+ window-switcher/
+ Expose.js new: layout()
+ test_expose.js new: node check of layout()
+ HotCorner.qml new: the corner window and its glow
+ Switcher.qml grid and key handler replaced by a positioning Repeater
+ WindowCard.qml external size, hover-only title and close
+ shell.qml instantiates HotCorner
+ README.md rewritten for the expose and the corner
+
+The Hyprland `blur-window-switcher` layer rule matches the overlay namespace;
+the corner window gets its own namespace, `quickshell-hot-corner`, and no blur
+rule.
+
+## Testing
+
+`node window-switcher/test_expose.js`, asserting:
+
+- seven identical 2560x1080 rects at the same position end with no two
+ overlapping, all inside W by H;
+- two runs on the same input give the same output;
+- a single window is scaled at most 0.5;
+- a 1076x1916 rect and a 2556x1076 rect keep their width and height ratios
+ after layout.
+
+On screen, by the user: the scatter, hover title and close, the glow
+completing and cancelling, the corner inert with `status.gaming` set.
+
+A new file and a new instantiation in `shell.qml` need a restart of the
+window-switcher shell, not a hot reload.