diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-18 12:26:10 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-18 12:26:10 +0200 |
| commit | 1f6bd402b5d2ffef845129a327cae95113b23e55 (patch) | |
| tree | b522d59b159b82de33eb8014dda815c2f5b62fa7 /window-switcher/Switcher.qml | |
| parent | a880134140f7ab8195c6651c2f64f614855c8dce (diff) | |
| download | quickshell-1f6bd402b5d2ffef845129a327cae95113b23e55.tar.gz quickshell-1f6bd402b5d2ffef845129a327cae95113b23e55.zip | |
window-switcher: animate, respace and label the expose, add arrow keys
Closing a window from the expose now glides the rest into their new
rects instead of jumping. The Repeater's plain array model rebuilt every
card on any change, leaving nothing to animate; ScriptModel diffs by
identity, so only the closed card goes. The Behaviors stay off until the
fade-in settles, or every card would fly in from the corner on open.
The scatter keeps its centre weighting but spaces windows further apart
(GAP 60 -> 200 real px) and may fill 95% of the screen rather than 92%.
Measured with node at 7 windows: about 45px on screen between cards.
The bottom 28px are reserved so the lowest card's name stays on screen.
Each card shows the app's desktop-entry name below its preview, falling
back to the app id, and the app icon moved to a larger overlay in the
preview's bottom-left corner; the hover title starts right of it.
Arrow keys select a card and Enter focuses it. The scatter has no grid,
so Expose.step picks the nearest centre on the arrow's side with
sideways drift counted double, and the first press picks the top-left
card; test_expose.js covers it. The selection is held as the window,
not an index, so a close that shifts the list does not move it, and
hovering selects too, so pointer and keys never show two highlights.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'window-switcher/Switcher.qml')
| -rw-r--r-- | window-switcher/Switcher.qml | 42 |
1 files changed, 39 insertions, 3 deletions
diff --git a/window-switcher/Switcher.qml b/window-switcher/Switcher.qml index e6eb792..1eebfa8 100644 --- a/window-switcher/Switcher.qml +++ b/window-switcher/Switcher.qml @@ -81,11 +81,16 @@ 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 { anchors.fill: parent focus: true Keys.onEscapePressed: root.close() + Keys.onReturnPressed: if (scatter.selected) root.focusAfterClose(scatter.selected) + Keys.onEnterPressed: if (scatter.selected) root.focusAfterClose(scatter.selected) + Keys.onLeftPressed: scatter.move(-1, 0) + Keys.onRightPressed: scatter.move(1, 0) + Keys.onUpPressed: scatter.move(0, -1) + Keys.onDownPressed: scatter.move(0, 1) Rectangle { anchors.fill: parent @@ -127,12 +132,14 @@ Scope { // Real sizes come from the raw hyprctl object. A window // that has none yet stands in at 1280x800 until it does. + // The bottom 28px are kept free for the lowest card's + // app name, which hangs below its preview. 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) + }), width, height - 28) // ponytail: a fade, not a flight from each window's real // position; add the flight if the fade feels abrupt. @@ -140,8 +147,30 @@ Scope { Component.onCompleted: opacity = 1 Behavior on opacity { NumberAnimation { duration: 150 } } + // The keyboard selection, held as the window rather + // than an index, so a close that shifts the list + // does not move it onto a neighbour. Hovering a card + // selects it too, so pointer and keys never show two. + property var selected: null + readonly property int selectedIndex: Windows.windows.indexOf(selected) + + function move(dx, dy) { + const i = Expose.step(rects, selectedIndex, dx, dy); + if (i >= 0) selected = Windows.windows[i]; + } + + // Cards glide to their new rects when the scatter + // changes, but not while it fades in, or every card + // would fly in from the corner on open. + property bool settled: false + onOpacityChanged: if (opacity === 1) settled = true + + // A plain array model would rebuild every card on any + // change, leaving nothing to animate. ScriptModel diffs + // by identity, so a closed window removes one delegate + // and the rest keep theirs. Repeater { - model: Windows.windows + model: ScriptModel { values: Windows.windows } WindowCard { required property int index @@ -156,6 +185,13 @@ Scope { width: rect.w height: rect.h + Behavior on x { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + Behavior on y { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + Behavior on width { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + Behavior on height { enabled: scatter.settled; NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } + + selected: scatter.selected === modelData + onHoveredChanged: if (hovered) scatter.selected = modelData onActivated: root.focusAfterClose(modelData) // Closing is a side errand, so the overlay // stays open: a dismissal would mean reopening |
