aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-15-expose-design.md
blob: c86bd7c2c4c9e3781045c5b399fcd32fb90581dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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.