# 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: [{w, h}] in real pixels 1. **Seed.** Window k starts on a golden-angle spiral (radius 0.45 times the tallest window times sqrt(k), angle k times 137.5°, x stretched by the screen's aspect), so the most recent window sits at the centre. Seeding from real positions was the first design and was dropped after a prototype: every window here fills its monitor, so all start in one pile, and pushing a pile apart made a line that still overlapped after 300 passes and filled 8 to 15% of the screen. The spiral converged in 1 to 55 passes with no overlap. Where a window lives carries no meaning in the scatter, by choice. 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 that costs less screen, overlap divided by the screen's 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. **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. 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 `size` yet gets 1280x800; 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 2556x1076 rects, and a mixed desk of fourteen, 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.