aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher/README.md
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-15 11:03:50 +0200
committerDanilo M. <danix@danix.xyz>2026-10-15 11:03:50 +0200
commit6cfda76ee4a859a068d10c457a21d67635f06386 (patch)
treeecaf8588ee0db2587aa39f1003e1e2deb1fff204 /window-switcher/README.md
parent00a3eb4047b76840233bb9ee872f20b7280ba427 (diff)
downloadquickshell-6cfda76ee4a859a068d10c457a21d67635f06386.tar.gz
quickshell-6cfda76ee4a859a068d10c457a21d67635f06386.zip
docs: the expose and its hot corner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'window-switcher/README.md')
-rw-r--r--window-switcher/README.md118
1 files changed, 81 insertions, 37 deletions
diff --git a/window-switcher/README.md b/window-switcher/README.md
index 516d8dd..66e6f69 100644
--- a/window-switcher/README.md
+++ b/window-switcher/README.md
@@ -1,29 +1,31 @@
# window-switcher
-Every open window as a live preview, in one centred grid over a dimmed screen.
-ALT+TAB opens it, a click or Enter picks a window, Escape drops it. It replaces
-a rofi list that showed the same windows as text.
+Every open window as a live preview, scattered over a dimmed DP-1 the way
+macOS Mission Control does it. ALT+TAB or the top-right corner of DP-1 opens
+it, a click picks a window, Escape drops it. It replaced a uniform grid, which
+replaced a rofi list.
┌──────────────────────────────────────────────────────────────┐
- │ │
- │ ┌────────────────────┐ ┌────────────────────┐ │
- │ │ [icon] [x] │ │ [icon] [x] │ │
- │ │ │ │ │ │
- │ │ live preview │ │ live preview │ │
- │ │ fitted in a │ │ │ │
- │ │ 16:10 box │ │ │ │
- │ └────────────────────┘ └────────────────────┘ │
- │ firefox kitty │
- │ a page title, elided... ~/Programming/GIT/... │
- │ [browser icon] [server icon] │
- │ │
+ │ ┌───────────────┐ ◜ │
+ │ │ [icon][ws] │ ┌────┐ ┌───────────────┐ │
+ │ │ │ │ │ │ │ │
+ │ └───────────────┘ │ │ │ (hovered) │ │
+ │ ┌───────────────┐ ┌──────────┐ │ │ │ title... [x]│ │
+ │ │ │ │ most │ │ │ └───────────────┘ │
+ │ │ │ │ recent │ └────┘ │
+ │ └───────────────┘ └──────────┘ ┌───────────────┐ │
+ │ │ │ │
+ │ └───────────────┘ │
└──────────────────────────────────────────────────────────────┘
-The cards are ordered most recently used first, so the window you just left is
-the first one, the same order ALT+TAB implies. Windows on a `special:`
-workspace are the scratchpad, which has its own bind, and are left out.
+Every preview keeps its window's real shape and its size relative to the
+others: one scale for all, so a portrait DP-3 window is visibly narrower and
+taller than a DP-1 one. The most recently used window sits at the centre and
+older ones spiral outwards. Where a window lives on its desktop carries no
+meaning here, by choice. Windows on a `special:` workspace are the scratchpad,
+which has its own bind, and are left out.
-Each card's desktop marker is an icon for that workspace rather than a bare
+Each preview's desktop marker is an icon for that workspace rather than a bare
number. The eight icons come from `Workspaces.qml`, a singleton in `shared/`
that this component links, as it does `Theme.qml`, and that the island's flick
uses too, so the two agree. The names are resolved through the active icon
@@ -52,15 +54,43 @@ shell to expand `~`. The bind lives in `keybindings.lua` on ALT+TAB. `show` and
| | |
| --- | --- |
| ALT+TAB | open, or close if already open |
-| click a card, or Enter | focus that window and dismiss |
-| arrows | move the selection, up and down by a row |
-| Tab / Shift+Tab | move the selection by one |
-| the `x` on a card | close that window, overlay stays open |
+| top-right corner of DP-1 | open, after the glow (see below) |
+| click a preview | focus that window and dismiss |
+| the `x` on a hovered preview | close that window, overlay stays open |
| Escape, or a click on the backdrop | dismiss, focus unchanged |
+The title and the `x` show only on hover, which also draws the accent border.
+There is no keyboard selection: this is a pointer feature, and the keyboard
+keeps only the way in and Escape.
+
Closing leaves the overlay up on purpose. Tidying several windows would
-otherwise mean reopening between each one, and the card goes when Hyprland says
-the window went, not when the button is pressed.
+otherwise mean reopening between each one, and the preview goes when Hyprland
+says the window went, not when the button is pressed. The rest then move to
+fill the space.
+
+## Hot corner
+
+`HotCorner.qml` is a 48x48 layer window in the top-right corner of DP-1, which
+is a true corner: DP-1 is the right-hand monitor, so the pointer stops there.
+Its input region is only the 2x2 pixels in the very corner. The rest is drawn
+but click-through, because a larger input area would swallow clicks meant for
+the windows underneath. That is also why it cannot light up while the pointer
+is merely approaching.
+
+Touching the corner grows a quarter-circle accent glow out of it over 250ms,
+and the expose opens when it is full. Leaving first shrinks it back and
+nothing opens. The dwell is what keeps a pointer passing through on its way to
+something else from triggering it.
+
+After it fires it re-arms only once the pointer leaves the corner while the
+expose is closed. Opening the expose covers the corner, which reads as
+leaving, so without that rule an Escape with the pointer still in the corner
+would reopen it 250ms later.
+
+It is hidden while `status.gaming` is set. It reads that file from
+`$XDG_RUNTIME_DIR` itself rather than linking `shared/Status.qml`, which is not
+passive: a second instance would run its own game poll and react to
+presentation mode a second time, pausing breaktimer twice.
## Hyprland dispatches from QML
@@ -107,25 +137,39 @@ that a dispatch requires, so `addressOf()` puts it back.
**`toplevel.wayland` can be null.** The Hyprland toplevel and its wlr handle
are separate objects created at different moments, so a window that is
-appearing or being destroyed has one and not the other. The card guards every
+appearing or being destroyed has one and not the other. The preview guards every
use of it.
-## Card geometry
+## The scatter
+
+`Expose.js` holds the layout, pure JavaScript tested under node:
-**A card is a fixed 16:10 box with the preview fitted inside**, not a box
-shaped to its preview. The previews are not one shape: DP-1 windows are near
-21:9 (2556x1034) and DP-3 reports `transform=1`, so its windows are portrait
-9:16 (1076x1916). Sizing each card to its own window gives ragged rows and
-breaks the alignment of the three text lines under them.
+ node window-switcher/test_expose.js
-**Card width is clamped by the height as well as the width.** A centred `Grid`
-has no way to scroll, so without the height term a busy desktop pushes rows off
-the top and bottom where they cannot be reached. The cards shrink instead.
-There is a ceiling on the width too, or a single window becomes a full-screen
+Windows are seeded on a golden-angle spiral, stretched to the screen's aspect,
+then overlapping pairs are pushed apart, each by half the overlap with a 60px
+real gap, until nothing overlaps. Thirty gravity rounds follow, each pulling
+every window 15% closer to the centre and pushing them apart again: without
+them the spiral's gaps stayed and the scatter read as loose, filling 24 to 30%
+of the screen; with them 35 to 55%. Twenty windows lay out in about 4ms. A single scale then fits the result into 92%
+of the screen, capped at 0.5 so a lone window does not become a full-screen
mirror of itself.
+**Seeding from real positions does not work on this desk.** It was the first
+design: every window here fills its monitor, so they all started at the same
+spot, 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.
+
+**The push axis is chosen against the screen's shape, not in raw pixels.** In
+pixels a wide window overlaps less vertically, so wide windows always
+separated up and down and stacked into a column on a 21:9 screen.
+
+**Real sizes come from `lastIpcObject.size`.** A window without one yet stands
+in at 1280x800 until it arrives; the layout is a binding and repaints.
+
**An uncaptured `ScreencopyView` reports `sourceSize` of `QSize(-1, -1)`**, not
-`(0, 0)`. The aspect ratio guard therefore tests for a positive height. The
+`(0, 0)`. The preview's aspect ratio guard therefore tests for a positive height. The
obvious defensive rewrites, `!== 0` or a plain truthiness check, all pass on
`-1` and produce a negative ratio.