// Window mode: outline every window on the focused workspace, highlight the one // under the pointer and dim the rest, click to capture it. // // Geometry comes from `hyprctl -j clients` via services/Capture.qml, because // Quickshell's HyprlandToplevel exposes no position or size. import QtQuick import qs.config import qs.services Item { id: root // Layout-global origin of the output this overlay covers. property real originX: 0 property real originY: 0 signal picked readonly property var windows: Capture.windows // Front-to-back hit test: Capture.windows is already sorted by // focusHistoryID, so the first hit is the topmost window. readonly property int hoveredIndex: { if (!area.containsMouse) return -1; const mx = area.mouseX; const my = area.mouseY; for (let i = 0; i < root.windows.length; i++) { const w = root.windows[i]; const x = w.x - root.originX; const y = w.y - root.originY; if (mx >= x && mx <= x + w.w && my >= y && my <= y + w.h) return i; } return -1; } // Once a window has been hovered it stays highlighted after the pointer // moves down onto the control bar, so the shutter button always has // something to shoot. Index 0 is the most recently focused window, which is // what GNOME preselects. property int stickyIndex: 0 onHoveredIndexChanged: { if (root.hoveredIndex >= 0) root.stickyIndex = root.hoveredIndex; } onWindowsChanged: root.stickyIndex = 0 readonly property int activeIndex: root.hoveredIndex >= 0 ? root.hoveredIndex : root.stickyIndex readonly property var hovered: (root.activeIndex >= 0 && root.activeIndex < root.windows.length) ? root.windows[root.activeIndex] : null // Layout-global rect of the hovered window, or null. The overlay hands this // straight to Capture.commit(). readonly property var selectedRect: root.hovered ? Qt.rect(root.hovered.x, root.hovered.y, root.hovered.w, root.hovered.h) : null Spotlight { anchors.fill: parent holeVisible: root.hovered !== null hole: root.hovered ? Qt.rect(root.hovered.x - root.originX, root.hovered.y - root.originY, root.hovered.w, root.hovered.h) : Qt.rect(0, 0, 0, 0) } // Faint outline on every candidate so the user can see what is pickable // before moving the pointer, the way GNOME's window shots do. Repeater { model: root.windows Frame { required property var modelData required property int index x: modelData.x - root.originX y: modelData.y - root.originY width: modelData.w height: modelData.h visible: index !== root.activeIndex strokeWidth: 1 strokeColor: Theme.alpha(Theme.fg, 0.35) } } Frame { visible: root.hovered !== null x: root.hovered ? root.hovered.x - root.originX : 0 y: root.hovered ? root.hovered.y - root.originY : 0 width: root.hovered ? root.hovered.w : 0 height: root.hovered ? root.hovered.h : 0 strokeWidth: 3 strokeColor: Theme.accent fillColor: Theme.alpha(Theme.accent, 0.08) } // Title chip on the highlighted window — stacked windows are otherwise very // hard to tell apart once the rest of the screen is dimmed. Rectangle { id: chip visible: root.hovered !== null x: (root.hovered ? root.hovered.x - root.originX : 0) + 12 y: (root.hovered ? root.hovered.y - root.originY : 0) + 12 implicitWidth: Math.min(chipText.implicitWidth + 24, root.hovered ? root.hovered.w - 24 : 0) implicitHeight: 32 radius: Theme.pillRadius border.width: 0 color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha) Text { id: chipText anchors.centerIn: parent width: chip.width - 24 text: root.hovered ? (root.hovered.title || root.hovered.appId) : "" color: Theme.fg elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall } } // Shown when hyprctl gave us nothing — a blank dimmed screen with no // explanation would just look broken. Text { anchors.centerIn: parent visible: root.windows.length === 0 text: "No windows to capture on this workspace" color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeLarge } MouseArea { id: area anchors.fill: parent hoverEnabled: true cursorShape: root.hovered ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: { if (root.hovered) root.picked(); } } }