aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-10-15-expose-design.md
blob: 488ab6af48e90e16fd9f0ced475bcb3d6b146a2c (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
133
134
135
136
137
138
# 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.