diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-15 11:03:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-15 11:03:49 +0200 |
| commit | 00a3eb4047b76840233bb9ee872f20b7280ba427 (patch) | |
| tree | 339acfe1e60890ac4d7582c29db4d590f71bb598 /window-switcher/Switcher.qml | |
| parent | 4cc85e59d7b3f53f4eec897f022bc4bcb643f185 (diff) | |
| download | quickshell-00a3eb4047b76840233bb9ee872f20b7280ba427.tar.gz quickshell-00a3eb4047b76840233bb9ee872f20b7280ba427.zip | |
feat(window-switcher): the expose replaces the grid, with a hot corner
Previews are placed at their Expose.js rects and sized to them, so each
keeps its window's shape and relative size. Title and close show on hover
only, so the scatter stays clean; icon and workspace marker always show.
The keyboard selection is gone: this is a pointer feature, and Escape is
the one key left.
The top-right corner of DP-1 opens it after a 250ms glow that grows out of
the corner, which is the warning and the way out. Input is only the corner's
2x2 pixels, since a larger input region would eat clicks on the windows
below. It re-arms only when the pointer leaves while the expose is closed,
or an Escape with the pointer still in the corner reopens it. It hides while
status.gaming is set, read straight from the file: linking shared/Status.qml
would start a second game poll and fire presentation side effects twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'window-switcher/Switcher.qml')
| -rw-r--r-- | window-switcher/Switcher.qml | 95 |
1 files changed, 29 insertions, 66 deletions
diff --git a/window-switcher/Switcher.qml b/window-switcher/Switcher.qml index d907ab7..bd5c607 100644 --- a/window-switcher/Switcher.qml +++ b/window-switcher/Switcher.qml @@ -12,6 +12,7 @@ import Quickshell import Quickshell.Wayland import QtQuick +import "Expose.js" as Expose Scope { id: root @@ -80,39 +81,11 @@ Scope { // Keys reach a focused item, not a window: Keys.onEscapePressed on // the PanelWindow above never fires. This item is what listens. + // Escape is the only key: the expose is a pointer feature. Item { - id: keyHandler anchors.fill: parent focus: true - - // The LazyLoader destroys the overlay when it closes and - // builds a new one on the next opening, verified by a build - // counter: two openings, two constructions. So this starts at - // 0 every time on its own, and there is nothing to reset. A - // Component.onCompleted assigning 0 here would be a no-op. - property int selectedIndex: 0 - - function commit() { - const list = Windows.windows; - if (selectedIndex >= 0 && selectedIndex < list.length) - root.focusAfterClose(list[selectedIndex]); - } - - function move(delta) { - const n = Windows.windows.length; - if (n === 0) return; - selectedIndex = (selectedIndex + delta + n) % n; - } - Keys.onEscapePressed: root.close() - Keys.onReturnPressed: commit() - Keys.onEnterPressed: commit() - Keys.onLeftPressed: move(-1) - Keys.onRightPressed: move(1) - Keys.onUpPressed: move(-grid.columns) - Keys.onDownPressed: move(grid.columns) - Keys.onTabPressed: move(1) - Keys.onBacktabPressed: move(-1) Rectangle { anchors.fill: parent @@ -146,38 +119,26 @@ Scope { } } - Grid { - id: grid - anchors.centerIn: parent - visible: Windows.windows.length > 0 - - readonly property int count: Windows.windows.length - readonly property int gap: 30 - readonly property int maxCardWidth: 640 - - // Columns are the window count capped at six, so one - // card sits dead centre and two straddle the middle. - columns: Math.max(1, Math.min(count, 6)) - - // Cards grow to fill their row, up to a ceiling. - // Without the ceiling a lone preview becomes a - // full-screen mirror of the window it stands for. - readonly property int rows: Math.ceil(count / columns) - - // The three text lines and the spacing above them, - // which the box height has to leave room for. - readonly property int textHeight: 54 - - // Clamped by the height as well as the width. Without - // the second term a busy desktop pushes rows off the - // top and bottom, and a centred Grid has no way to - // scroll to them, so the cards shrink instead. - readonly property int cardWidth: Math.min( - maxCardWidth, - Math.floor((parent.width * 0.92 - (columns - 1) * gap) / columns), - Math.floor(((parent.height * 0.92 - (rows - 1) * gap) / rows - textHeight) * 16 / 10)) + // The scatter. Recomputed whenever the window list or a + // window's size changes, so closing one moves the rest. + Item { + id: scatter + anchors.fill: parent - spacing: gap + // Real sizes come from the raw hyprctl object. A window + // that has none yet stands in at 1280x800 until it does. + readonly property var rects: Expose.layout( + Windows.windows.map(t => { + const o = t.lastIpcObject; + const s = o && o.size; + return s ? { w: s[0], h: s[1] } : { w: 1280, h: 800 }; + }), width, height) + + // ponytail: a fade, not a flight from each window's real + // position; add the flight if the fade feels abrupt. + opacity: 0 + Component.onCompleted: opacity = 1 + Behavior on opacity { NumberAnimation { duration: 150 } } Repeater { model: Windows.windows @@ -186,14 +147,16 @@ Scope { required property int index required property var modelData + readonly property var rect: scatter.rects[index] + ?? { x: 0, y: 0, w: 0, h: 0 } + toplevel: modelData - cardWidth: grid.cardWidth - selected: index === keyHandler.selectedIndex + x: rect.x + y: rect.y + width: rect.w + height: rect.h - onActivated: { - keyHandler.selectedIndex = index; - keyHandler.commit(); - } + onActivated: root.focusAfterClose(modelData) // Closing is a side errand, so the overlay // stays open: a dismissal would mean reopening // to close a second window. The card goes when |
