diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-15 11:03:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-15 11:03:49 +0200 |
| commit | 00a3eb4047b76840233bb9ee872f20b7280ba427 (patch) | |
| tree | 339acfe1e60890ac4d7582c29db4d590f71bb598 /window-switcher/WindowCard.qml | |
| parent | 4cc85e59d7b3f53f4eec897f022bc4bcb643f185 (diff) | |
| download | quickshell-00a3eb4047b76840233bb9ee872f20b7280ba427.tar.gz quickshell-00a3eb4047b76840233bb9ee872f20b7280ba427.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'window-switcher/WindowCard.qml')
| -rw-r--r-- | window-switcher/WindowCard.qml | 135 |
1 files changed, 67 insertions, 68 deletions
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 + } } } } |
