diff --git a/Modules/Bar/Components/TrayIcons.qml b/Modules/Bar/Components/TrayIcons.qml index 6fe8ac7..fae91a7 100644 --- a/Modules/Bar/Components/TrayIcons.qml +++ b/Modules/Bar/Components/TrayIcons.qml @@ -42,7 +42,7 @@ WidgetBase { Repeater { id: repeater - model: SystemTray.items + model: SystemTray.items.values.filter(i => i.status !== Status.Passive && i.hasMenu) TrayItem { id: trayItem diff --git a/Modules/Settings/Common/WallpaperCropper.qml b/Modules/Settings/Common/WallpaperCropper.qml index b2fea36..68076b0 100644 --- a/Modules/Settings/Common/WallpaperCropper.qml +++ b/Modules/Settings/Common/WallpaperCropper.qml @@ -26,10 +26,10 @@ Item { const cropRect = cropRectLoader.item; - const cropXPercent = (cropRect.x - cropRect.imageX) / scaledImg.paintedWidth; - const cropYPercent = (cropRect.y - cropRect.imageY) / scaledImg.paintedHeight; - const cropWidthPercent = cropRect.width / scaledImg.paintedWidth; - const cropHeightPercent = cropRect.height / scaledImg.paintedHeight; + const cropXPercent = (cropRect.x - cropRect.imageX) / cropRect.imageW; + const cropYPercent = (cropRect.y - cropRect.imageY) / cropRect.imageH; + const cropWidthPercent = cropRect.width / cropRect.imageW; + const cropHeightPercent = cropRect.height / cropRect.imageH; Wallpapers.setCrop(currentScreen.name, Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent), cropRect.zoom); } @@ -79,16 +79,13 @@ Item { visible: opacity > 0 Behavior on opacity { - Anim { - } + Anim {} } Behavior on scale { - Anim { - } + Anim {} } Behavior on y { - Anim { - } + Anim {} } Component.onCompleted: { @@ -99,34 +96,10 @@ Item { Qt.callLater(syncCropToScreen); } - IconButton { - anchors.margins: Tokens.padding.small - anchors.right: parent.right - anchors.top: parent.top - icon: "check" - opacity: wrapper.changesMade ? 1 : 0 - scale: wrapper.changesMade ? 1 : 0 - z: 2 - - Behavior on opacity { - Anim { - } - } - Behavior on scale { - Anim { - } - } - - onClicked: { - wrapper.applyCrop(); - wrapper.changesMade = false; - } - } - ButtonRow { id: screenSelector - anchors.bottom: scaledImg.top + anchors.bottom: imageSlot.top anchors.bottomMargin: Tokens.spacing.medium anchors.left: parent.left anchors.leftMargin: Tokens.padding.extraLarge * 2 @@ -149,6 +122,7 @@ Item { isRound: true shapeMorph: true text: modelData.name + font: Tokens.font.body.builders.large.weight(Font.DemiBold).build() onClicked: wrapper.selectScreen(index) } @@ -178,16 +152,52 @@ Item { onInteraction: value => { wrapper.zoomClipRect(1 + (value * 4)); - wrapper.changesMade = true; + } + + onPosChanged: wrapper.changesMade = true + } + + ButtonRow { + spacing: Tokens.spacing.medium + + IconTextButton { + isRound: true + icon: "close" + text: qsTr("Cancel") + font: Tokens.font.body.large + shapeMorph: true + horizontalPadding: Tokens.padding.large + inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) + inactiveOnColor: Colors.palette.m3onSurfaceVariant + enabled: wrapper.changesMade + + onClicked: { + wrapper.syncCropToScreen(); + wrapper.changesMade = false; + } + } + + IconTextButton { + isRound: true + icon: "check" + text: qsTr("Apply") + font: Tokens.font.body.large + shapeMorph: true + horizontalPadding: Tokens.padding.large + inactiveColor: Colors.palette.m3secondary + inactiveOnColor: Colors.palette.m3onSecondary + enabled: wrapper.changesMade + + onClicked: { + wrapper.applyCrop(); + wrapper.changesMade = false; + } } } } - Image { - id: scaledImg - - property var displayData - property real monitorScale: 1.0 + Item { + id: imageSlot anchors.bottom: sliderLayout.top anchors.bottomMargin: Tokens.spacing.medium @@ -195,147 +205,225 @@ Item { anchors.right: parent.right anchors.top: parent.top anchors.topMargin: wrapper.buttonRowHeight + Tokens.spacing.medium - // anchors.top: screenSelector.bottom - asynchronous: true - fillMode: Image.PreserveAspectFit - retainWhileLoading: true - source: Wallpapers.current - sourceSize.height: parent.height - sourceSize.width: parent.width - onPaintedWidthChanged: { - if (paintedWidth > 0 && cropRectLoader.item) { - if (!wrapper.screens.length) - wrapper.refreshScreens(); + CustomClippingRect { + id: fitBox - wrapper.syncCropToScreen(); - } - } - onSourceChanged: { - if (cropRectLoader.item) { - if (!wrapper.screens.length) - wrapper.refreshScreens(); + readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1 + readonly property real fitW: Math.min(imageSlot.width, imageSlot.height * aspect) - wrapper.syncCropToScreen(); - } - } - onStatusChanged: { - if (scaledImg.status == Image.Ready && cropRectLoader.item) { - if (!wrapper.screens.length) - wrapper.refreshScreens(); + implicitHeight: Math.round(fitW / aspect) + implicitWidth: Math.round(fitW) + x: Math.round((imageSlot.width - width) / 2) + y: Math.round((imageSlot.height - height) / 2) + radius: Tokens.rounding.largeIncreased - wrapper.syncCropToScreen(); - } - } + Loader { + anchors.centerIn: parent - Loader { - id: cropRectLoader + opacity: scaledImg.status === Image.Ready ? 0 : 1 + active: opacity > 0 - active: scaledImg.status === Image.Ready + sourceComponent: CustomRect { + implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2 + implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2 - sourceComponent: Component { - CustomRect { - id: cropRect + color: Colors.palette.m3primaryContainer + radius: Tokens.rounding.full - property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1 - readonly property real baseHeight: baseWidth / aspectRatio - readonly property real baseWidth: { - let fittedHeight = scaledImg.paintedHeight; - let fittedWidth = fittedHeight * aspectRatio; + LoadingIndicator { + id: loadingIndicator - if (fittedWidth > scaledImg.paintedWidth) { - fittedWidth = scaledImg.paintedWidth; - fittedHeight = fittedWidth / aspectRatio; - } + anchors.centerIn: parent - return fittedWidth; + containsIcon: true + implicitSize: Math.min(fitBox.width, fitBox.height) * 0.3 } - readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 - readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 - property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight - property real zoom: 1.0 + } - function centerInImage() { - x = imageX + (scaledImg.paintedWidth - width) / 2; - y = imageY + (scaledImg.paintedHeight - height) / 2; + Behavior on opacity { + Anim { + type: Anim.DefaultEffects } - - function clampToBounds() { - x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); - y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); - } - - function restoreFromData() { - let data = Wallpapers.getCrop(wrapper.currentScreen.name); - - if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) { - zoom = data.zoom > 0 ? data.zoom : 1.0; - x = imageX + (data.x * scaledImg.paintedWidth); - y = imageY + (data.y * scaledImg.paintedHeight); - - clampToBounds(); - } else { - zoom = 1.0; - centerInImage(); - } - } - - border.color: Colors.palette.m3primary - border.width: 2 - height: baseHeight / zoom - opacity: 1 - width: baseWidth / zoom - - Behavior on opacity { - Anim { - } - } - - Component.onCompleted: { - restoreFromData(); - } - onHeightChanged: clampToBounds() - onWidthChanged: clampToBounds() } } - } - MouseArea { - id: mouse + Image { + id: scaledImg - function updateCrop(mouseX, mouseY) { - if (!cropRectLoader.item) - return; + anchors.fill: parent + asynchronous: true + fillMode: Image.Stretch + source: Wallpapers.current + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + retainWhileLoading: true + opacity: status === Image.Ready ? 1 : 0 - const cropRect = cropRectLoader.item; - - let nx = mouseX - cropRect.width * 0.5; - let ny = mouseY - cropRect.height * 0.5; - - nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + scaledImg.paintedWidth - cropRect.width)); - ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + scaledImg.paintedHeight - cropRect.height)); - - cropRect.x = nx; - cropRect.y = ny; + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } } - anchors.fill: parent - hoverEnabled: true - preventStealing: true + Loader { + id: cropRectLoader - onPositionChanged: mouse => { - if (pressed) { + active: scaledImg.status === Image.Ready + + sourceComponent: Component { + CustomRect { + id: cropRect + + property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1 + readonly property real baseHeight: baseWidth / aspectRatio + + readonly property real baseWidth: { + let fittedHeight = imageH; + let fittedWidth = fittedHeight * aspectRatio; + + if (fittedWidth > imageW) { + fittedWidth = imageW; + fittedHeight = fittedWidth / aspectRatio; + } + + return fittedWidth; + } + + readonly property real imageH: Math.round(fitBox.height) + readonly property real imageW: Math.round(fitBox.width) + readonly property real imageX: 0 + readonly property real imageY: 0 + property real imgAspectRatio: imageH > 0 ? imageW / imageH : 1 + property real zoom: 1.0 + + function centerInImage() { + x = Math.round(imageX + (imageW - width) / 2); + y = Math.round(imageY + (imageH - height) / 2); + } + + function clampToBounds() { + x = Math.max(imageX, Math.min(x, imageX + imageW - width)); + y = Math.max(imageY, Math.min(y, imageY + imageH - height)); + } + + function restoreFromData() { + let data = Wallpapers.getCrop(wrapper.currentScreen.name); + + if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) { + zoom = data.zoom > 0 ? data.zoom : 1.0; + x = Math.round(imageX + (data.x * imageW)); + y = Math.round(imageY + (data.y * imageH)); + + clampToBounds(); + } else { + zoom = 1.0; + centerInImage(); + } + } + + height: Math.round(baseHeight / zoom) + width: Math.round(baseWidth / zoom) + color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent" + + Behavior on color { + CAnim {} + } + + Component.onCompleted: { + restoreFromData(); + } + onHeightChanged: clampToBounds() + onWidthChanged: clampToBounds() + } + } + } + + ShaderEffect { + anchors.fill: parent + + readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1 + + function snap(v: real): real { + return Math.round(v * dpr) / dpr; + } + + property color borderColor: Colors.palette.m3tertiary + property vector4d cropBox: cropRectLoader.item ? Qt.vector4d(snap(cropRectLoader.item.x), snap(cropRectLoader.item.y), snap(cropRectLoader.item.width), snap(cropRectLoader.item.height)) : Qt.vector4d(0, 0, 0, 0) + property real borderWidth: 2 + property real cornerRadius: fitBox.radius + property vector2d itemSize: Qt.vector2d(width, height) + property real pixelScale: dpr + + fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/crop_sdf.frag.qsb`) + } + + MouseArea { + id: mouse + + property real grabX: 0 + property real grabY: 0 + + function insideCropRect(mx, my): bool { + const r = cropRectLoader.item; + if (!r) + return false; + + return mx >= r.x && mx <= r.x + r.width && my >= r.y && my <= r.y + r.height; + } + + function beginGrab(mx, my): void { + const r = cropRectLoader.item; + if (!r) + return; + + const inside = insideCropRect(mx, my); + + if (inside) { + grabX = mx - r.x; + grabY = my - r.y; + } else { + grabX = r.width * 0.5; + grabY = r.height * 0.5; + } + } + + function updateCrop(mx, my): void { + if (!cropRectLoader.item) + return; + + const cropRect = cropRectLoader.item; + + let nx = mx - grabX; + let ny = my - grabY; + + nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + cropRect.imageW - cropRect.width)); + ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + cropRect.imageH - cropRect.height)); + + cropRect.x = nx; + cropRect.y = ny; + } + + anchors.fill: parent + hoverEnabled: true + preventStealing: true + + cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor + + onPositionChanged: mouse => { + if (pressed) { + updateCrop(mouse.x, mouse.y); + wrapper.changesMade = true; + } + } + onPressed: mouse => { + beginGrab(mouse.x, mouse.y); updateCrop(mouse.x, mouse.y); - wrapper.changesMade = true; } } - onPressed: mouse => { - updateCrop(mouse.x, mouse.y); - wrapper.changesMade = true; - } - onReleased: { - wrapper.changesMade = true; - } } } } diff --git a/assets/shaders/crop_sdf.frag b/assets/shaders/crop_sdf.frag new file mode 100644 index 0000000..bbe2aa0 --- /dev/null +++ b/assets/shaders/crop_sdf.frag @@ -0,0 +1,40 @@ +#version 440 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec2 itemSize; + vec4 cropBox; + vec4 borderColor; + float cornerRadius; + float borderWidth; + float pixelScale; +}; + +float sdRoundBox(vec2 p, vec2 halfSize, float r) { + vec2 q = abs(p) - halfSize + r; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; +} + +void main() { + vec2 sz = itemSize * pixelScale; + vec2 p = qt_TexCoord0 * sz; + vec4 box = cropBox * pixelScale; + float r = cornerRadius * pixelScale; + float bw = borderWidth * pixelScale; + + float dImage = sdRoundBox(p - sz * 0.5, sz * 0.5, r); + float dCrop = sdRoundBox(p - (box.xy + box.zw * 0.5), box.zw * 0.5, 0.0); + + float d = max(dImage, dCrop); + + // ramp is exactly 1 physical pixel wide + float outer = clamp(0.5 - d, 0.0, 1.0); + float inner = clamp(0.5 - (d + bw), 0.0, 1.0); + float a = outer - inner; + + fragColor = vec4(borderColor.rgb, 1.0) * (borderColor.a * a * qt_Opacity); +} diff --git a/assets/shaders/crop_sdf.frag.qsb b/assets/shaders/crop_sdf.frag.qsb new file mode 100644 index 0000000..786ccc0 Binary files /dev/null and b/assets/shaders/crop_sdf.frag.qsb differ diff --git a/shell.qml b/shell.qml index f602e71..60ec9ed 100644 --- a/shell.qml +++ b/shell.qml @@ -3,7 +3,7 @@ // @ pragma DefaultEnv QSG_RHI_BACKEND=vulkan //@ pragma DefaultEnv QSG_NO_VSYNC=1 //@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 -//@ pragma DefaultEnv QT_SCALE_FACTOR_ROUNDING_POLICY=Round +// @ pragma DefaultEnv QT_SCALE_FACTOR_ROUNDING_POLICY=Round //@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 //@ pragma DropExpensiveFonts import Quickshell @@ -23,33 +23,27 @@ ShellRoot { settings.watchFiles: true - Drawers { - } + Drawers {} - Wallpaper { - } + Wallpaper {} - AreaPicker { - } + AreaPicker {} Lock { id: lock } - Shortcuts { - } + Shortcuts {} IdleMonitors { lock: lock } - Polkit { - } + Polkit {} LazyLoader { activeAsync: root.laptop - component: BatteryService { - } + component: BatteryService {} } }