diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-12 18:49:57 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-12 18:49:57 +0200 |
| commit | 63c27fbf5dbe438a74773a733a8e20acc1988de3 (patch) | |
| tree | b24f72cf809f832297e18cfc87d62d1ce0748f15 /window-switcher | |
| parent | dda3ebf3f50e62e397836a79b58da2ad3150f7fc (diff) | |
| download | quickshell-63c27fbf5dbe438a74773a733a8e20acc1988de3.tar.gz quickshell-63c27fbf5dbe438a74773a733a8e20acc1988de3.zip | |
feat(window-switcher): one card, with the preview fitted
The card box is a fixed 16:10 and the preview is fitted inside it rather
than the card taking its preview's shape. DP-1 windows are near 21:9 and
DP-3 reports transform=1, so its windows are portrait 9:16: a 4.4x span
of aspect ratio in one grid, and ragged rows would break the alignment of
the three text lines that make it scannable.
Both corner overlays carry a scrim. They are drawn over an arbitrary
window, so a themed icon can otherwise land on a same-coloured region and
disappear.
An uncaptured ScreencopyView reports sourceSize (-1, -1) rather than
(0, 0), so the aspect ratio guard tests for a positive height.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S
Diffstat (limited to 'window-switcher')
| -rw-r--r-- | window-switcher/WindowCard.qml | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/window-switcher/WindowCard.qml b/window-switcher/WindowCard.qml new file mode 100644 index 0000000..de026e8 --- /dev/null +++ b/window-switcher/WindowCard.qml @@ -0,0 +1,136 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import QtQuick + +Column { + id: card + + required property var toplevel + property bool selected: false + property int cardWidth: 380 + + signal activated() + signal closeRequested() + + // A Hyprland toplevel carries its Wayland handle separately, and the two + // are not created in the same instant, so this reads null for a window + // that is appearing or going away. Everything below guards for it. + readonly property var wl: toplevel.wayland ?? null + + width: cardWidth + spacing: 8 + + // 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) + radius: 10 + color: Theme.surface + border.color: card.selected ? Theme.accent : Theme.surfaceAlt + border.width: card.selected ? 3 : 1 + + 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 + width: Math.min(parent.width - 8, (parent.height - 8) * ar) + height: Math.min(parent.height - 8, (parent.width - 8) / ar) + } + + MouseArea { + anchors.fill: parent + 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 { + 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) + + IconImage { + anchors.centerIn: parent + width: 24; height: 24 + source: Quickshell.iconPath(card.wl ? card.wl.appId : "", true) + } + } + + Rectangle { + id: closeButton + anchors { right: parent.right; top: parent.top; margins: 8 } + width: 34; height: 34; radius: 17 + color: closeHover.hovered + ? Theme.red + : Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.75) + + IconImage { + anchors.centerIn: parent + width: 22; height: 22 + source: Quickshell.iconPath("window-close", true) + } + + HoverHandler { id: closeHover } + + MouseArea { + anchors.fill: parent + 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 + } + + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: "[" + (card.toplevel.workspace ? card.toplevel.workspace.name : "?") + "]" + color: Theme.accent + font.family: Theme.fontFamily + font.pixelSize: 13 + } +} |
