feat: screenshot tool

This commit is contained in:
2 * r + 2 * t 2025-06-19 15:00:06 +10:00
parent b0a0b6d322
commit 53cea2beb3
3 changed files with 326 additions and 0 deletions

View file

@ -0,0 +1,70 @@
import "root:/widgets"
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
Scope {
LazyLoader {
id: root
property bool freeze
Variants {
model: Quickshell.screens
StyledWindow {
id: win
required property ShellScreen modelData
screen: modelData
name: "area-picker"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
Picker {
loader: root
screen: win.modelData
}
}
}
}
IpcHandler {
target: "picker"
function open(): void {
root.freeze = false;
root.activeAsync = true;
}
function openFreeze(): void {
root.freeze = true;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshot"
description: "Open screenshot tool"
onPressed: {
root.freeze = false;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshotFreeze"
description: "Open screenshot tool (freeze mode)"
onPressed: {
root.freeze = true;
root.activeAsync = true;
}
}
}

View file

@ -0,0 +1,254 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import QtQuick.Effects
MouseArea {
id: root
required property LazyLoader loader
required property ShellScreen screen
property int borderWidth
property real rounding
property real ssx
property real ssy
property real sx: 0
property real sy: 0
property real ex: screen.width
property real ey: screen.height
property real rsx: Math.min(sx, ex)
property real rsy: Math.min(sy, ey)
property real sw: Math.abs(sx - ex)
property real sh: Math.abs(sy - ey)
property list<var> clients: Hyprland.clients.filter(c => c.workspace.id === Hyprland.activeWsId)
function checkClientRects(x: real, y: real): void {
for (const c of clients) {
if (c.x <= x && c.y <= y && c.x + c.width >= x && c.y + c.height >= y) {
sx = c.x;
sy = c.y;
ex = c.x + c.width;
ey = c.y + c.height;
break;
}
}
}
anchors.fill: parent
opacity: 0
hoverEnabled: true
Component.onCompleted: {
// Break binding if frozen
if (loader.freeze)
clients = clients;
opacity = 1;
sx = screen.width / 2 - 100;
sy = screen.height / 2 - 100;
ex = screen.width / 2 + 100;
ey = screen.height / 2 + 100;
}
onPressed: event => {
ssx = event.x;
ssy = event.y;
}
onReleased: {
if (closeAnim.running)
return;
Quickshell.execDetached(["sh", "-c", `grim -l 0 -g '${Math.ceil(rsx)},${Math.ceil(rsy)} ${Math.floor(sw)}x${Math.floor(sh)}' - | swappy -f -`]);
closeAnim.start();
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (pressed) {
sx = ssx;
sy = ssy;
ex = x;
ey = y;
} else {
checkClientRects(x, y);
}
}
focus: true
Keys.onEscapePressed: closeAnim.start()
SequentialAnimation {
id: closeAnim
ParallelAnimation {
Anim {
target: root
property: "opacity"
to: 0
duration: Appearance.anim.durations.large
}
Anim {
target: root
properties: "rsx,rsy"
to: 0
}
Anim {
target: root
property: "sw"
to: root.screen.width
}
Anim {
target: root
property: "sh"
to: root.screen.height
}
}
PropertyAction {
target: root.loader
property: "activeAsync"
value: false
}
}
Connections {
target: Hyprland
function onActiveWsIdChanged(): void {
root.checkClientRects(root.mouseX, root.mouseY);
}
}
Process {
running: true
command: ["hyprctl", "-j", "getoption", "general:border_size"]
stdout: StdioCollector {
onStreamFinished: root.borderWidth = JSON.parse(text).int
}
}
Process {
running: true
command: ["hyprctl", "-j", "getoption", "decoration:rounding"]
stdout: StdioCollector {
onStreamFinished: root.rounding = JSON.parse(text).int
}
}
Loader {
anchors.fill: parent
active: root.loader.freeze
asynchronous: true
sourceComponent: ScreencopyView {
captureSource: root.screen
}
}
StyledRect {
id: background
anchors.fill: parent
color: Colours.palette.m3secondaryContainer
visible: false
}
Item {
id: selectionWrapper
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
id: selectionRect
radius: root.rounding
x: root.rsx
y: root.rsy
implicitWidth: root.sw
implicitHeight: root.sh
}
}
MultiEffect {
anchors.fill: parent
source: background
maskSource: selectionWrapper
maskEnabled: true
maskInverted: true
maskSpreadAtMin: 1
maskThresholdMin: 0.5
opacity: 0.3
}
Rectangle {
color: "transparent"
radius: root.rounding > 0 ? root.rounding + root.borderWidth : 0
border.width: root.borderWidth
border.color: Colours.palette.m3primary
x: selectionRect.x - root.borderWidth
y: selectionRect.y - root.borderWidth
implicitWidth: selectionRect.implicitWidth + root.borderWidth * 2
implicitHeight: selectionRect.implicitHeight + root.borderWidth * 2
Behavior on border.color {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on rsx {
enabled: !root.pressed
Anim {}
}
Behavior on rsy {
enabled: !root.pressed
Anim {}
}
Behavior on sw {
enabled: !root.pressed
Anim {}
}
Behavior on sh {
enabled: !root.pressed
Anim {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}

View file

@ -1,11 +1,13 @@
import "modules" import "modules"
import "modules/drawers" import "modules/drawers"
import "modules/background" import "modules/background"
import "modules/areapicker"
import Quickshell import Quickshell
ShellRoot { ShellRoot {
Background {} Background {}
Drawers {} Drawers {}
AreaPicker {}
Shortcuts {} Shortcuts {}
} }