diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-12 18:53:34 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-12 18:53:34 +0200 |
| commit | 0f3d2ebfb4aed4801548c29cb1e7fe52e9246a90 (patch) | |
| tree | dfd92d1064e74a44bdedb0a860a60546057cbe8c /window-switcher/Switcher.qml | |
| parent | 63c27fbf5dbe438a74773a733a8e20acc1988de3 (diff) | |
| download | quickshell-0f3d2ebfb4aed4801548c29cb1e7fe52e9246a90.tar.gz quickshell-0f3d2ebfb4aed4801548c29cb1e7fe52e9246a90.zip | |
feat(window-switcher): the grid, the selection, and an empty state
Columns are the window count capped at six and the grid centres, so one
card sits dead centre and two straddle the middle. Cards grow to fill
their row up to 640px: without a ceiling a lone preview becomes a
full-screen mirror of the window it stands for.
The close button leaves the overlay open, because closing is a side
errand and a dismissal would mean reopening to close a second window.
That makes an empty overlay reachable by closing the last window, so
there is an empty state rather than a full-screen dim with nothing in it,
which reads as a hang.
The selection needs no reset. The LazyLoader destroys the overlay on
close and constructs a new one on the next opening, verified with a build
counter: two openings, two constructions. selectedIndex is therefore 0 on
every showing by initialisation, and an explicit assignment in
Component.onCompleted would be a no-op.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
Diffstat (limited to 'window-switcher/Switcher.qml')
| -rw-r--r-- | window-switcher/Switcher.qml | 108 |
1 files changed, 100 insertions, 8 deletions
diff --git a/window-switcher/Switcher.qml b/window-switcher/Switcher.qml index d989e57..4d01b90 100644 --- a/window-switcher/Switcher.qml +++ b/window-switcher/Switcher.qml @@ -25,6 +25,10 @@ Scope { function toggle() { root.open = !root.open; } function close() { root.open = false; } + // Hyprland.toplevels reads empty until it is asked, and a window opened + // since the last showing would otherwise be missing from the grid. + onOpenChanged: if (open) Windows.refresh(); + // A config with no visible window exits, reporting nothing. This 1x1 // click-through window is what holds the shell open while the overlay // is hidden, which is almost always. @@ -54,14 +58,41 @@ Scope { // Keys reach a focused item, not a window: Keys.onEscapePressed on // the PanelWindow above never fires. This item is what listens. Item { + id: keyHandler anchors.fill: parent focus: true - Keys.onEscapePressed: { - console.log("SWITCHER escape"); - root.close(); + // 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) { + Windows.focus(list[selectedIndex]); + root.close(); + } + } + + 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 color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.82) @@ -71,12 +102,73 @@ Scope { onClicked: root.close() } - Text { + // Nothing to switch to. A full-screen dim with nothing in + // it reads as a hang, so it says so instead. + Column { + anchors.centerIn: parent + spacing: 14 + visible: Windows.windows.length === 0 + + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Nothing here but hopes and dreams..." + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: 26 + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "go create something beautiful!" + color: Theme.overlay + font.family: Theme.fontFamily + font.pixelSize: 32 + } + } + + Grid { + id: grid anchors.centerIn: parent - text: "press Escape" - color: Theme.text - font.family: Theme.fontFamily - font.pixelSize: 28 + 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 cardWidth: Math.min( + maxCardWidth, + Math.floor((parent.width * 0.92 - (columns - 1) * gap) / columns)) + + spacing: gap + + Repeater { + model: Windows.windows + + WindowCard { + required property int index + required property var modelData + + toplevel: modelData + cardWidth: grid.cardWidth + selected: index === keyHandler.selectedIndex + + onActivated: { + keyHandler.selectedIndex = index; + keyHandler.commit(); + } + // Closing is a side errand, so the overlay + // stays open: a dismissal would mean reopening + // to close a second window. The card goes when + // Hyprland says it went. + onCloseRequested: Windows.closeWindow(modelData) + } + } } } } |
