From ad36b4ed8f9ff2ddbffc33b17b0ee7464ffffeb3 Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 30 Sep 2026 23:33:22 +0200 Subject: [PATCH] fix: sizing and positioning of portrait aspect ratios for wallpaper cropper --- Modules/Settings/Common/WallpaperCropper.qml | 152 +++++++++---------- 1 file changed, 72 insertions(+), 80 deletions(-) diff --git a/Modules/Settings/Common/WallpaperCropper.qml b/Modules/Settings/Common/WallpaperCropper.qml index 0b5b0d9..70418ee 100644 --- a/Modules/Settings/Common/WallpaperCropper.qml +++ b/Modules/Settings/Common/WallpaperCropper.qml @@ -13,9 +13,7 @@ import qs.Services Item { id: wrapper - readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null - readonly property real maxZoom: 5.0 - + property int buttonRowHeight: Tokens.padding.large * 3 readonly property bool changesMade: { cropRevision; @@ -27,21 +25,13 @@ Item { return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001; } - property int cropRevision: 0 - property int buttonRowHeight: Tokens.padding.large * 3 + readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null + readonly property real maxZoom: 5.0 property var screens: [] property int selectedScreenIndex: 0 property bool shouldBeActive: true - function zoomFromSlider(t: real): real { - return 1 / (1 - t * (1 - 1 / maxZoom)); - } - - function sliderFromZoom(zoom: real): real { - return (1 - 1 / zoom) / (1 - 1 / maxZoom); - } - function applyCrop(): void { if (!cropRectLoader.item || !currentScreen) return; @@ -72,6 +62,10 @@ Item { Qt.callLater(syncCropToScreen); } + function sliderFromZoom(zoom: real): real { + return (1 - 1 / zoom) / (1 - 1 / maxZoom); + } + function syncCropToScreen(): void { if (cropRectLoader.item) cropRectLoader.item.restoreFromData(); @@ -94,6 +88,10 @@ Item { cropRect.clampToBounds(); } + function zoomFromSlider(t: real): real { + return 1 / (1 - t * (1 - 1 / maxZoom)); + } + anchors.left: parent.left anchors.right: parent.right implicitHeight: shouldBeActive ? 430 : 0 @@ -102,13 +100,16 @@ Item { visible: opacity > 0 Behavior on opacity { - Anim {} + Anim { + } } Behavior on scale { - Anim {} + Anim { + } } Behavior on y { - Anim {} + Anim { + } } Component.onCompleted: { @@ -140,12 +141,12 @@ Item { required property var modelData fillWidth: true + font: Tokens.font.body.builders.large.weight(Font.DemiBold).build() inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7) inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7) isRound: true shapeMorph: true text: modelData.name - font: Tokens.font.body.builders.large.weight(Font.DemiBold).build() onClicked: wrapper.selectScreen(index) } @@ -180,15 +181,15 @@ Item { spacing: Tokens.spacing.medium IconTextButton { - isRound: true - icon: "close" - text: qsTr("Cancel") + enabled: wrapper.changesMade font: Tokens.font.body.large - shapeMorph: true horizontalPadding: Tokens.padding.large + icon: "close" inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) inactiveOnColor: Colors.palette.m3onSurfaceVariant - enabled: wrapper.changesMade + isRound: true + shapeMorph: true + text: qsTr("Cancel") onClicked: { wrapper.syncCropToScreen(); @@ -196,15 +197,15 @@ Item { } IconTextButton { - isRound: true - icon: "check" - text: qsTr("Apply") + enabled: wrapper.changesMade font: Tokens.font.body.large - shapeMorph: true horizontalPadding: Tokens.padding.large + icon: "check" inactiveColor: Colors.palette.m3secondary inactiveOnColor: Colors.palette.m3onSecondary - enabled: wrapper.changesMade + isRound: true + shapeMorph: true + text: qsTr("Apply") onClicked: { wrapper.applyCrop(); @@ -227,42 +228,39 @@ Item { id: fitBox readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1 - readonly property real fitW: Math.min(imageSlot.width, imageSlot.height * aspect) + readonly property real fitW: imageSlot.height * aspect + anchors.centerIn: parent 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 + onAspectChanged: console.log(aspect) + Loader { - anchors.centerIn: parent - - opacity: scaledImg.status === Image.Ready ? 0 : 1 active: opacity > 0 + anchors.centerIn: parent + opacity: scaledImg.status === Image.Ready ? 0 : 1 + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } sourceComponent: CustomRect { - implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2 - implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2 - color: Colors.palette.m3primaryContainer + implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2 + implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2 radius: Tokens.rounding.full LoadingIndicator { id: loadingIndicator anchors.centerIn: parent - containsIcon: true implicitSize: Math.min(fitBox.width, fitBox.height) * 0.3 } } - - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } } Image { @@ -270,14 +268,12 @@ Item { 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 + fillMode: Image.PreserveAspectFit + mipmap: true opacity: status === Image.Ready ? 1 : 0 + retainWhileLoading: true + smooth: true + source: Wallpapers.current Behavior on opacity { Anim { @@ -297,7 +293,6 @@ Item { 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; @@ -309,7 +304,6 @@ Item { return fittedWidth; } - readonly property real imageH: Math.round(fitBox.height) readonly property real imageW: Math.round(fitBox.width) readonly property real imageX: 0 @@ -327,6 +321,13 @@ Item { y = Math.max(imageY, Math.min(y, imageY + imageH - height)); } + function restoreFromData() { + const t = savedTarget(); + zoom = t.zoom; + x = t.x; + y = t.y; + } + // Where the rect should be according to saved data (same rules as a restore). function savedTarget() { const data = Wallpapers.getCrop(wrapper.currentScreen.name); @@ -349,19 +350,13 @@ Item { }; } - function restoreFromData() { - const t = savedTarget(); - zoom = t.zoom; - x = t.x; - y = t.y; - } - + color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent" 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 {} + CAnim { + } } Component.onCompleted: { @@ -374,21 +369,19 @@ Item { } ShaderEffect { - anchors.fill: parent - + property color borderColor: Colors.palette.m3tertiary + property real borderWidth: 2 + property real cornerRadius: fitBox.radius + 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) readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1 + property vector2d itemSize: Qt.vector2d(width, height) + property real pixelScale: dpr 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 - + anchors.fill: parent fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/crop_sdf.frag.qsb`) } @@ -398,14 +391,6 @@ Item { 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) @@ -422,6 +407,14 @@ Item { } } + 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 updateCrop(mx, my): void { if (!cropRectLoader.item) return; @@ -439,11 +432,10 @@ Item { } anchors.fill: parent + cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor hoverEnabled: true preventStealing: true - cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor - onPositionChanged: mouse => { if (pressed) { updateCrop(mouse.x, mouse.y);