// Copyright (C) 2026 Danilo M. // // 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 // 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 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 // The bar's icon for this window's workspace, empty for a named workspace. // Resolved to a path here so the card can fall back to the number when the // active icon theme has no such icon, rather than showing a blank marker. readonly property string workspaceIconPath: { const name = Windows.workspaceIcon(toplevel.workspace); return name !== "" ? Quickshell.iconPath(name, true) : ""; } HoverHandler { id: cardHover } Rectangle { id: box anchors.fill: parent radius: 10 color: Theme.surface 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 readonly property real ar: sourceSize.height > 0 ? sourceSize.width / sourceSize.height : 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) } MouseArea { anchors.fill: parent onClicked: card.activated() } // 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 } spacing: 6 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 ? Qt.lighter(Theme.red, 1.25) : 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 { id: closeArea anchors.fill: parent onClicked: card.closeRequested() } } 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 { 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 } } } }