pragma ComponentBehavior: Bound import ZShell import Quickshell import Quickshell.Wayland import QtQuick import QtQuick.Effects import qs.Components import qs.Config import qs.Helpers.Picker import qs.Paths Item { id: root property list clients: { const mon = Hypr.monitorFor(screen); if (!mon) return []; const special = mon.lastIpcObject.specialWorkspace; const wsId = special.name ? special.id : mon.activeWorkspace.id; return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => { const ac = a.lastIpcObject; const bc = b.lastIpcObject; return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating); }); } readonly property int cornerRadius: Hypr.options.decoration.rounding property real ex: screen.width property real ey: screen.height property color inkColor: "#ff3b30" required property LazyLoader loader property string mode: "select" property bool onClient property real realBorderWidth: 2 property real realRounding: onClient ? (Hypr.options.decoration.rounding ?? 0) : 0 property real rsx: Math.min(sx, ex) property real rsy: Math.min(sy, ey) readonly property real scaleRatio: Hypr.monitorFor(screen).scale required property ShellScreen screen property real sh: Math.abs(sy - ey) readonly property color shadowColor: Hypr.options.decoration.shadow.color readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled readonly property var shadowOffset: Hypr.options.decoration.shadow.offset readonly property int shadowRange: Hypr.options.decoration.shadow.range readonly property int shadowRenderPower: Hypr.options.decoration.shadow.render_power property real ssx property real ssy property real sw: Math.abs(sx - ex) property real sx: 0 property real sy: 0 property string tool: "move" function cancel(): void { annotations.clearAll(); closeAnim.start(); } function checkClientRects(x: real, y: real): void { for (const client of clients) { if (!client) continue; let { at: [cx, cy], size: [cw, ch] } = client.lastIpcObject; cx -= screen.x; cy -= screen.y; if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) { onClient = true; sx = cx; sy = cy; ex = cx + cw; ey = cy + ch; break; } } } function enterEditMode(): void { mode = "edit"; tool = "move"; } function save(copyToClipboard: bool): void { const rounding = root.cornerRadius > 0; const shadow_blur = root.shadowRange / root.shadowRenderPower; const r = Math.floor(root.shadowColor.r * 256); const g = Math.floor(root.shadowColor.g * 256); const b = Math.floor(root.shadowColor.b * 256); const a = Math.floor(root.shadowColor.a * 256); const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`); const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : []; const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null; overlay.visible = border.visible = false; handles.visible = false; toolbar.visible = false; ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => { const finalize = finalPath => { if (copyToClipboard) { Quickshell.execDetached(["sh", "-c", `wl-copy --type image/png < '${finalPath.replace(/'/g, `'\\''`)}'`]); } if (!copyToClipboard || Config.screenshot.saveOnCopy) { const dest = `${Paths.screenshots}/zshell-${Date.now()}.png`; Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname '${dest}')" && cp '${path.replace(/'/g, `'\\''`)}' '${dest}'`]); } }; if (postCmd) { Quickshell.execDetached(postCmd.concat([path])); finalize(path); } else { finalize(path); } }); closeAnim.start(); } anchors.fill: parent focus: true opacity: 0 Behavior on opacity { Anim { duration: 300 } } Behavior on rsx { enabled: !selectArea.pressed && root.mode === "select" ExAnim { } } Behavior on rsy { enabled: !selectArea.pressed && root.mode === "select" ExAnim { } } Behavior on sh { enabled: !selectArea.pressed && root.mode === "select" ExAnim { } } Behavior on sw { enabled: !selectArea.pressed && root.mode === "select" ExAnim { } } Component.onCompleted: { Hypr.extras.refreshOptions(); screencopy.active = true; opacity = 1; const c = clients[0]; if (c) { const cx = c.lastIpcObject.at[0] - screen.x; const cy = c.lastIpcObject.at[1] - screen.y; onClient = true; sx = cx; sy = cy; ex = cx + c.lastIpcObject.size[0]; ey = cy + c.lastIpcObject.size[1]; } else { sx = screen.width / 2 - 100; sy = screen.height / 2 - 100; ex = screen.width / 2 + 100; ey = screen.height / 2 + 100; } } Keys.onEscapePressed: root.mode === "edit" ? cancel() : closeAnim.start() Keys.onPressed: e => { if ((e.key == Qt.Key_C) && (e.modifiers & Qt.ControlModifier)) { save(true); e.accepted = true; } else if ((e.key == Qt.Key_S) && (e.modifiers & Qt.ControlModifier)) { save(false); e.accepted = true; } } onClientsChanged: { if (root.mode === "select") checkClientRects(selectArea.mouseX, selectArea.mouseY); } MouseArea { id: selectArea anchors.fill: parent cursorShape: Qt.CrossCursor enabled: root.mode === "select" hoverEnabled: true visible: root.mode === "select" onPositionChanged: event => { const x = event.x; const y = event.y; if (pressed) { root.onClient = false; root.sx = root.ssx; root.sy = root.ssy; root.ex = x; root.ey = y; } else { root.checkClientRects(x, y); } } onPressed: event => { root.ssx = event.x; root.ssy = event.y; } onReleased: root.enterEditMode() } SequentialAnimation { id: closeAnim PropertyAction { property: "closing" target: root.loader value: true } ParallelAnimation { Anim { duration: 300 property: "opacity" target: root to: 0 } ExAnim { properties: "rsx,rsy" target: root to: 0 } ExAnim { property: "sw" target: root to: root.screen.width } ExAnim { property: "sh" target: root to: root.screen.height } } PropertyAction { property: "activeAsync" target: root.loader value: false } } Item { id: captureLayer anchors.fill: parent Loader { id: screencopy active: false anchors.fill: parent asynchronous: true sourceComponent: ScreencopyView { captureSource: root.screen paintCursor: false } } AnnotationCanvas { id: annotations anchors.fill: parent inkColor: root.inkColor layer.enabled: true tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none" layer.effect: MultiEffect { maskEnabled: true maskSource: selectionWrapper maskSpreadAtMin: 1 maskThresholdMin: 0.5 } } } Rectangle { id: overlay anchors.fill: parent color: "white" layer.enabled: true opacity: 0.3 radius: root.realRounding visible: screencopy.item?.hasContent ?? false layer.effect: MultiEffect { maskEnabled: true maskInverted: true maskSource: selectionWrapper maskSpreadAtMin: 1 maskThresholdMin: 0.5 } } Item { id: selectionWrapper anchors.fill: parent layer.enabled: true visible: false Rectangle { id: selectionRect implicitHeight: root.sh implicitWidth: root.sw radius: root.realRounding x: root.rsx y: root.rsy } } Rectangle { id: border border.color: DynamicColors.palette.m3primary border.width: root.realBorderWidth color: "transparent" implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 visible: screencopy.item?.hasContent ?? false x: selectionRect.x - root.realBorderWidth y: selectionRect.y - root.realBorderWidth Behavior on border.color { Anim { } } } SelectionHandles { id: handles selH: root.sh selW: root.sw selX: root.rsx selY: root.rsy visible: root.mode === "edit" && root.tool === "move" onBoundsChanged: (x, y, w, h) => { root.sx = x; root.sy = y; root.ex = x + w; root.ey = y + h; } onMoveBy: (dx, dy) => { const w = root.sw, h = root.sh; let nx = root.rsx + dx, ny = root.rsy + dy; nx = Math.max(0, Math.min(nx, root.screen.width - w)); ny = Math.max(0, Math.min(ny, root.screen.height - h)); root.sx = nx; root.sy = ny; root.ex = nx + w; root.ey = ny + h; } } AnnotationToolbar { id: toolbar inkColor: root.inkColor tool: root.tool visible: root.mode === "edit" x: Math.max(8, Math.min(root.rsx + root.sw / 2 - width / 2, root.screen.width - width - 8)) y: (root.rsy + root.sh + height + Appearance.padding.normal <= root.screen.height) ? root.rsy + root.sh + Appearance.padding.normal : (root.rsy - height - Appearance.padding.normal <= 0) ? root.rsy + root.sh - height - Appearance.padding.normal : root.rsy - height - Appearance.padding.normal onCancelRequested: root.cancel() onClearRequested: annotations.clearAll() onColorSelected: c => root.inkColor = c onCopyRequested: root.save(true) onSaveRequested: root.save(false) onToolSelected: t => root.tool = t onUndoRequested: annotations.undo() } component ExAnim: Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects } }