Super+Tab already cycled windows, but nothing was drawn, so you chose
blind and could only confirm the choice by arriving. A visible switcher
is muscle memory for anyone arriving from macOS or GNOME, and it was the
last item of roadmap phase 03 that did not need coordination.
Ordered most-recently-used, not by creation, because that is what makes
the gesture useful: one Tab returns to the window you just came from.
Hyprland does not report an MRU order, so it is tracked from focus
changes and keyed by address, which is the only property stable for a
window's lifetime.
The gesture needs three binds rather than two. Tab steps the selection,
and the switch is committed on Super RELEASE -- the only way the
compositor can say the gesture is over. That bind is on the bare
modifier, so it fires on every Super release in the session; commit()
returns immediately when nothing is open, which is what makes it
affordable.
A list of names rather than thumbnails: at a glance you are looking for
"the other terminal", and a row of live previews is slower to read and
far more expensive to draw than this gesture deserves.
The interesting part is the bug. The overlay was built, mapped nothing,
and logged absolutely nothing -- because it declared `required property
var screen` while Variants supplies `modelData`. shell.qml has carried a
comment warning about exactly this since the Bar hit it, and I read that
comment earlier in the same session and still walked into it. A comment
that does not stop the person who read it is an argument for a test, so
per-screen-surface-contract now checks every per-screen delegate takes
its screen from modelData. Verified it catches the exact mistake.
Also fixes a regression from 8be3fc2: settings-pages-contract still
required vitalsIntervalMs on Home, where it no longer is. That contract
was pinning the split-across-two-pages arrangement the same commit
fixed, and I pushed without running it.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
130 lines
5.4 KiB
QML
130 lines
5.4 KiB
QML
// The Alt-Tab overlay.
|
|
//
|
|
// Deliberately a list of names rather than thumbnails: at a glance you are
|
|
// looking for "the other terminal", and a row of small live previews is both
|
|
// slower to read and considerably more expensive to draw than the gesture
|
|
// deserves. The dock already renders app identity this way, so the two agree.
|
|
//
|
|
// Only present while a switch is in progress -- there is nothing to keep alive
|
|
// between gestures, and a hidden always-loaded overlay is a surface that can
|
|
// go wrong while nobody is looking at it.
|
|
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
import qs.widgets
|
|
|
|
Loader {
|
|
id: root
|
|
|
|
// Plain `modelData`, not `required property var screen`. Variants supplies
|
|
// modelData, and shell.qml's own comment warns about exactly this: declaring
|
|
// `required property var screen` means the screen never resolves, the window
|
|
// is constructed and silently never maps, and NOTHING is logged. Bar and
|
|
// Dock both take the screen this way.
|
|
property var modelData: null
|
|
|
|
active: WindowSwitcherState.open
|
|
asynchronous: false
|
|
|
|
sourceComponent: PanelWindow {
|
|
screen: root.modelData
|
|
// Overlay so it sits above the focused window it is describing.
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.namespace: "qs-switcher"
|
|
// Nothing here is clickable: the gesture is driven entirely from the
|
|
// keyboard, and taking input would steal focus from the compositor
|
|
// mid-switch, which is the one thing that would break it.
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
exclusionMode: ExclusionMode.Ignore
|
|
color: "transparent"
|
|
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: Math.min(560, parent.width - 96)
|
|
implicitHeight: layout.implicitHeight + 24
|
|
radius: Theme.cardRadius
|
|
color: Theme.alpha(Theme.bgPopover, 0.97)
|
|
border.width: 1
|
|
border.color: Theme.alpha(Theme.fg, 0.09)
|
|
|
|
Column {
|
|
id: layout
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 12
|
|
spacing: 2
|
|
|
|
Repeater {
|
|
model: WindowSwitcherState.windows
|
|
|
|
Rectangle {
|
|
id: row
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property bool current: row.index === WindowSwitcherState.index
|
|
readonly property string appId: row.modelData?.wayland?.appId ?? ""
|
|
readonly property var entry: DesktopEntries.heuristicLookup(row.appId)
|
|
|
|
width: parent.width
|
|
height: 44
|
|
radius: 10
|
|
border.width: 0
|
|
color: row.current ? Theme.alpha(Theme.accent, 0.20) : "transparent"
|
|
|
|
Image {
|
|
id: icon
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 24
|
|
height: 24
|
|
sourceSize.width: 24
|
|
sourceSize.height: 24
|
|
source: row.entry?.icon ? Quickshell.iconPath(row.entry.icon, true) : ""
|
|
visible: source !== ""
|
|
}
|
|
|
|
Text {
|
|
anchors.left: icon.visible ? icon.right : parent.left
|
|
anchors.leftMargin: icon.visible ? 12 : 14
|
|
anchors.right: appName.left
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
// A window with no title yet is still a window you
|
|
// can switch to; naming it after its application is
|
|
// better than an empty row.
|
|
text: row.modelData?.title || row.entry?.name || row.appId
|
|
elide: Text.ElideRight
|
|
color: row.current ? Theme.fg : Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: row.current ? Font.DemiBold : Font.Normal
|
|
}
|
|
|
|
Text {
|
|
id: appName
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: (row.entry?.name ?? "") !== "" && row.entry.name !== row.modelData?.title
|
|
text: row.entry?.name ?? ""
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|