From 00a3eb4047b76840233bb9ee872f20b7280ba427 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 15 Oct 2026 11:03:49 +0200 Subject: feat(window-switcher): the expose replaces the grid, with a hot corner Previews are placed at their Expose.js rects and sized to them, so each keeps its window's shape and relative size. Title and close show on hover only, so the scatter stays clean; icon and workspace marker always show. The keyboard selection is gone: this is a pointer feature, and Escape is the one key left. The top-right corner of DP-1 opens it after a 250ms glow that grows out of the corner, which is the warning and the way out. Input is only the corner's 2x2 pixels, since a larger input region would eat clicks on the windows below. It re-arms only when the pointer leaves while the expose is closed, or an Escape with the pointer still in the corner reopens it. It hides while status.gaming is set, read straight from the file: linking shared/Status.qml would start a second game poll and fire presentation side effects twice. Co-Authored-By: Claude Opus 5.5 --- window-switcher/WindowCard.qml | 135 ++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 68 deletions(-) (limited to 'window-switcher/WindowCard.qml') diff --git a/window-switcher/WindowCard.qml b/window-switcher/WindowCard.qml index b671f0f..177aac9 100644 --- a/window-switcher/WindowCard.qml +++ b/window-switcher/WindowCard.qml @@ -14,12 +14,13 @@ import Quickshell.Wayland import Quickshell.Widgets import QtQuick -Column { +// One window in the expose: a live preview sized from outside to its rect in +// the scatter. App icon and workspace marker are always shown; title and +// close only on hover, so the scatter stays clean. +Item { id: card required property var toplevel - property bool selected: false - property int cardWidth: 380 signal activated() signal closeRequested() @@ -37,34 +38,29 @@ Column { return name !== "" ? Quickshell.iconPath(name, true) : ""; } - width: cardWidth - spacing: 8 + HoverHandler { id: cardHover } - // A fixed 16:10 box with the preview fitted inside it. The previews are - // not one shape: DP-1 windows are near 21:9 and DP-3 is rotated, so its - // windows are portrait. Sizing each card to its own preview would give - // ragged rows and break the alignment of the text lines below. Rectangle { id: box - width: card.cardWidth - height: Math.round(card.cardWidth * 10 / 16) + anchors.fill: parent radius: 10 color: Theme.surface - border.color: card.selected ? Theme.accent : Theme.surfaceAlt - border.width: card.selected ? 3 : 1 + border.color: cardHover.hovered ? Theme.accent : Theme.surfaceAlt + border.width: cardHover.hovered ? 3 : 1 + // The rect already has the window's shape, so the preview fills it. + // The fit is kept for the moment before a capture lands: an + // uncaptured view reports sourceSize (-1, -1) rather than (0, 0), so + // this tests for a positive height, falling back to the box's ratio. ScreencopyView { id: preview anchors.centerIn: parent captureSource: card.wl live: true - // An uncaptured view reports sourceSize (-1, -1) rather than - // (0, 0), so this tests for a positive height rather than a - // non-zero one, and falls back to the box's own ratio. readonly property real ar: sourceSize.height > 0 ? sourceSize.width / sourceSize.height - : 16 / 10 + : parent.width / Math.max(1, parent.height) width: Math.min(parent.width - 8, (parent.height - 8) * ar) height: Math.min(parent.height - 8, (parent.width - 8) / ar) } @@ -74,24 +70,53 @@ Column { onClicked: card.activated() } - // Both corner overlays sit on an arbitrary window preview, so each - // carries its own scrim: a themed icon can otherwise land on a - // same-coloured region and vanish, a light icon on a white page being - // the obvious case. - Rectangle { + // Every overlay sits on an arbitrary window preview, so each carries + // its own scrim: a themed icon can otherwise land on a same-coloured + // region and vanish, a light icon on a white page being the obvious + // case. + Row { anchors { left: parent.left; top: parent.top; margins: 8 } - width: 34; height: 34; radius: 8 - color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.75) + spacing: 6 - IconImage { - anchors.centerIn: parent - width: 24; height: 24 - source: Quickshell.iconPath(card.wl ? card.wl.appId : "", true) + Rectangle { + width: 34; height: 34; radius: 8 + color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.75) + + IconImage { + anchors.centerIn: parent + width: 24; height: 24 + source: Quickshell.iconPath(card.wl ? card.wl.appId : "", true) + } + } + + // The desktop marker, drawn with the workspace icon rather than + // the number. A workspace with no icon keeps the name, so the + // marker never goes blank. + Rectangle { + width: Math.max(34, wsName.implicitWidth + 12); height: 34; radius: 8 + color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.75) + + IconImage { + anchors.centerIn: parent + width: 22; height: 22 + source: card.workspaceIconPath + } + + Text { + id: wsName + anchors.centerIn: parent + visible: card.workspaceIconPath === "" + text: card.toplevel.workspace ? card.toplevel.workspace.name : "?" + color: Theme.accent + font.family: Theme.fontFamily + font.pixelSize: 13 + } } } Rectangle { id: closeButton + visible: cardHover.hovered anchors { right: parent.right; top: parent.top; margins: 8 } width: 34; height: 34; radius: 17 color: closeArea.pressed @@ -114,48 +139,22 @@ Column { onClicked: card.closeRequested() } } - } - - Text { - width: parent.width - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - text: card.wl ? card.wl.appId : "" - color: Theme.text - font.family: Theme.fontFamily - font.pixelSize: 16 - font.bold: true - } - Text { - width: parent.width - elide: Text.ElideRight - text: card.toplevel.title - color: Theme.subtext - font.family: Theme.fontFamily - font.pixelSize: 13 - } - - // The desktop marker, drawn with waybar's workspace icon rather than the - // number. A workspace with no icon keeps the number, so the line never - // goes blank. - Item { - width: card.width - height: 24 - - IconImage { - anchors.centerIn: parent - width: 22; height: 22 - source: card.workspaceIconPath - } + Rectangle { + visible: cardHover.hovered + anchors { left: parent.left; right: parent.right; bottom: parent.bottom; margins: 8 } + height: 30; radius: 8 + color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.75) - Text { - anchors.centerIn: parent - visible: card.workspaceIconPath === "" - text: "[" + (card.toplevel.workspace ? card.toplevel.workspace.name : "?") + "]" - color: Theme.accent - font.family: Theme.fontFamily - font.pixelSize: 13 + Text { + anchors { fill: parent; leftMargin: 10; rightMargin: 10 } + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + text: card.toplevel.title + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: 13 + } } } } -- cgit v1.2.3