// 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" // Keyboard focus stays with the compositor: the gesture is driven by // binds, and taking focus mid-switch is the one thing that would break // stepping. Pointer input is a different matter -- see below. WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore color: "transparent" anchors { top: true; bottom: true; left: true; right: true } // Anywhere outside the card puts the switcher away. // // This is recovery, not decoration. The gesture commits on SUPER // release, which is a compositor bind running `qs ipc call` -- and if // that call ever fails to land, the overlay used to stay up with no // keyboard focus, no Escape handler and nothing clickable, which meant // the only way out was an IPC call from a terminal the overlay was // covering. MouseArea { anchors.fill: parent onClicked: WindowSwitcherState.cancel() } // A switch nobody finished. Ten seconds is far longer than the gesture // takes and far shorter than "forever", so a lost commit costs a pause // rather than the session. Timer { running: WindowSwitcherState.open interval: 10000 onTriggered: WindowSwitcherState.cancel() } 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: rowHover.hovered || row.current ? Theme.alpha(Theme.accent, row.current ? 0.20 : 0.10) : "transparent" // Pointing at a window and clicking it is the obvious // thing to try, and it did nothing. HoverHandler { id: rowHover } MouseArea { anchors.fill: parent onClicked: WindowSwitcherState.selectAt(row.index) } 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 } } } } } } }