aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher/Switcher.qml
diff options
context:
space:
mode:
Diffstat (limited to 'window-switcher/Switcher.qml')
-rw-r--r--window-switcher/Switcher.qml42
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