diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-15 10:21:29 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-15 10:21:29 +0200 |
| commit | 875bda45d04004815bb89e3e9f735af439e7fefd (patch) | |
| tree | 7b7db05bb0e044db2c59f81d218a208c23cc33cd /docs/superpowers/specs | |
| parent | b87ca1ab1edd1eb49594f7bad31904307a44b3fc (diff) | |
| download | quickshell-875bda45d04004815bb89e3e9f735af439e7fefd.tar.gz quickshell-875bda45d04004815bb89e3e9f735af439e7fefd.zip | |
docs(spec): expose on ALT+TAB and a hot corner
The window-switcher grid becomes a Mission Control scatter: previews keep
their real shape and relative size, seeded from their real positions and
pushed apart, then fitted with one scale. A top-right hot corner on DP-1
opens it after a 250ms glow that warns before it fires, and stays inert
while gaming. shared/Status.qml is not linked into window-switcher because
a second instance would run its own game poll and react to presentation
twice; the corner reads status.gaming directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/specs')
| -rw-r--r-- | docs/superpowers/specs/2026-10-15-expose-design.md | 132 |
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. |
