aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher/WindowCard.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-15 11:03:49 +0200
committerDanilo M. <danix@danix.xyz>2026-10-15 11:03:49 +0200
commit00a3eb4047b76840233bb9ee872f20b7280ba427 (patch)
tree339acfe1e60890ac4d7582c29db4d590f71bb598 /window-switcher/WindowCard.qml
parent4cc85e59d7b3f53f4eec897f022bc4bcb643f185 (diff)
downloadquickshell-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.qml135
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
+ }
}
}
}