fix: sizing and positioning of portrait aspect ratios for wallpaper cropper
This commit is contained in:
@@ -13,9 +13,7 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
|
property int buttonRowHeight: Tokens.padding.large * 3
|
||||||
readonly property real maxZoom: 5.0
|
|
||||||
|
|
||||||
readonly property bool changesMade: {
|
readonly property bool changesMade: {
|
||||||
cropRevision;
|
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;
|
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 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 var screens: []
|
||||||
property int selectedScreenIndex: 0
|
property int selectedScreenIndex: 0
|
||||||
property bool shouldBeActive: true
|
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 {
|
function applyCrop(): void {
|
||||||
if (!cropRectLoader.item || !currentScreen)
|
if (!cropRectLoader.item || !currentScreen)
|
||||||
return;
|
return;
|
||||||
@@ -72,6 +62,10 @@ Item {
|
|||||||
Qt.callLater(syncCropToScreen);
|
Qt.callLater(syncCropToScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sliderFromZoom(zoom: real): real {
|
||||||
|
return (1 - 1 / zoom) / (1 - 1 / maxZoom);
|
||||||
|
}
|
||||||
|
|
||||||
function syncCropToScreen(): void {
|
function syncCropToScreen(): void {
|
||||||
if (cropRectLoader.item)
|
if (cropRectLoader.item)
|
||||||
cropRectLoader.item.restoreFromData();
|
cropRectLoader.item.restoreFromData();
|
||||||
@@ -94,6 +88,10 @@ Item {
|
|||||||
cropRect.clampToBounds();
|
cropRect.clampToBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zoomFromSlider(t: real): real {
|
||||||
|
return 1 / (1 - t * (1 - 1 / maxZoom));
|
||||||
|
}
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitHeight: shouldBeActive ? 430 : 0
|
implicitHeight: shouldBeActive ? 430 : 0
|
||||||
@@ -102,13 +100,16 @@ Item {
|
|||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {}
|
Anim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {}
|
Anim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
Anim {}
|
Anim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -140,12 +141,12 @@ Item {
|
|||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
fillWidth: true
|
fillWidth: true
|
||||||
|
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
|
||||||
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
|
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
|
||||||
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
||||||
isRound: true
|
isRound: true
|
||||||
shapeMorph: true
|
shapeMorph: true
|
||||||
text: modelData.name
|
text: modelData.name
|
||||||
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
|
|
||||||
|
|
||||||
onClicked: wrapper.selectScreen(index)
|
onClicked: wrapper.selectScreen(index)
|
||||||
}
|
}
|
||||||
@@ -180,15 +181,15 @@ Item {
|
|||||||
spacing: Tokens.spacing.medium
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
IconTextButton {
|
IconTextButton {
|
||||||
isRound: true
|
enabled: wrapper.changesMade
|
||||||
icon: "close"
|
|
||||||
text: qsTr("Cancel")
|
|
||||||
font: Tokens.font.body.large
|
font: Tokens.font.body.large
|
||||||
shapeMorph: true
|
|
||||||
horizontalPadding: Tokens.padding.large
|
horizontalPadding: Tokens.padding.large
|
||||||
|
icon: "close"
|
||||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||||
enabled: wrapper.changesMade
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
wrapper.syncCropToScreen();
|
wrapper.syncCropToScreen();
|
||||||
@@ -196,15 +197,15 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconTextButton {
|
IconTextButton {
|
||||||
isRound: true
|
enabled: wrapper.changesMade
|
||||||
icon: "check"
|
|
||||||
text: qsTr("Apply")
|
|
||||||
font: Tokens.font.body.large
|
font: Tokens.font.body.large
|
||||||
shapeMorph: true
|
|
||||||
horizontalPadding: Tokens.padding.large
|
horizontalPadding: Tokens.padding.large
|
||||||
|
icon: "check"
|
||||||
inactiveColor: Colors.palette.m3secondary
|
inactiveColor: Colors.palette.m3secondary
|
||||||
inactiveOnColor: Colors.palette.m3onSecondary
|
inactiveOnColor: Colors.palette.m3onSecondary
|
||||||
enabled: wrapper.changesMade
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
text: qsTr("Apply")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
wrapper.applyCrop();
|
wrapper.applyCrop();
|
||||||
@@ -227,42 +228,39 @@ Item {
|
|||||||
id: fitBox
|
id: fitBox
|
||||||
|
|
||||||
readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1
|
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)
|
implicitHeight: Math.round(fitW / aspect)
|
||||||
implicitWidth: Math.round(fitW)
|
implicitWidth: Math.round(fitW)
|
||||||
x: Math.round((imageSlot.width - width) / 2)
|
|
||||||
y: Math.round((imageSlot.height - height) / 2)
|
|
||||||
radius: Tokens.rounding.largeIncreased
|
radius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
|
onAspectChanged: console.log(aspect)
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
opacity: scaledImg.status === Image.Ready ? 0 : 1
|
|
||||||
active: opacity > 0
|
active: opacity > 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: scaledImg.status === Image.Ready ? 0 : 1
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
sourceComponent: CustomRect {
|
sourceComponent: CustomRect {
|
||||||
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
|
||||||
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
|
||||||
|
|
||||||
color: Colors.palette.m3primaryContainer
|
color: Colors.palette.m3primaryContainer
|
||||||
|
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||||
|
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||||
radius: Tokens.rounding.full
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
LoadingIndicator {
|
LoadingIndicator {
|
||||||
id: loadingIndicator
|
id: loadingIndicator
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
containsIcon: true
|
containsIcon: true
|
||||||
implicitSize: Math.min(fitBox.width, fitBox.height) * 0.3
|
implicitSize: Math.min(fitBox.width, fitBox.height) * 0.3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@@ -270,14 +268,12 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.Stretch
|
fillMode: Image.PreserveAspectFit
|
||||||
source: Wallpapers.current
|
mipmap: true
|
||||||
sourceSize: {
|
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
|
||||||
return Qt.size(width * dpr, height * dpr);
|
|
||||||
}
|
|
||||||
retainWhileLoading: true
|
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
|
retainWhileLoading: true
|
||||||
|
smooth: true
|
||||||
|
source: Wallpapers.current
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
@@ -297,7 +293,6 @@ Item {
|
|||||||
|
|
||||||
property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1
|
property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1
|
||||||
readonly property real baseHeight: baseWidth / aspectRatio
|
readonly property real baseHeight: baseWidth / aspectRatio
|
||||||
|
|
||||||
readonly property real baseWidth: {
|
readonly property real baseWidth: {
|
||||||
let fittedHeight = imageH;
|
let fittedHeight = imageH;
|
||||||
let fittedWidth = fittedHeight * aspectRatio;
|
let fittedWidth = fittedHeight * aspectRatio;
|
||||||
@@ -309,7 +304,6 @@ Item {
|
|||||||
|
|
||||||
return fittedWidth;
|
return fittedWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real imageH: Math.round(fitBox.height)
|
readonly property real imageH: Math.round(fitBox.height)
|
||||||
readonly property real imageW: Math.round(fitBox.width)
|
readonly property real imageW: Math.round(fitBox.width)
|
||||||
readonly property real imageX: 0
|
readonly property real imageX: 0
|
||||||
@@ -327,6 +321,13 @@ Item {
|
|||||||
y = Math.max(imageY, Math.min(y, imageY + imageH - height));
|
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).
|
// Where the rect should be according to saved data (same rules as a restore).
|
||||||
function savedTarget() {
|
function savedTarget() {
|
||||||
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
|
||||||
@@ -349,19 +350,13 @@ Item {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreFromData() {
|
color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent"
|
||||||
const t = savedTarget();
|
|
||||||
zoom = t.zoom;
|
|
||||||
x = t.x;
|
|
||||||
y = t.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
height: Math.round(baseHeight / zoom)
|
height: Math.round(baseHeight / zoom)
|
||||||
width: Math.round(baseWidth / zoom)
|
width: Math.round(baseWidth / zoom)
|
||||||
color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent"
|
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
CAnim {}
|
CAnim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -374,21 +369,19 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShaderEffect {
|
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
|
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 {
|
function snap(v: real): real {
|
||||||
return Math.round(v * dpr) / dpr;
|
return Math.round(v * dpr) / dpr;
|
||||||
}
|
}
|
||||||
|
|
||||||
property color borderColor: Colors.palette.m3tertiary
|
anchors.fill: parent
|
||||||
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`)
|
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/crop_sdf.frag.qsb`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,14 +391,6 @@ Item {
|
|||||||
property real grabX: 0
|
property real grabX: 0
|
||||||
property real grabY: 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 {
|
function beginGrab(mx, my): void {
|
||||||
const r = cropRectLoader.item;
|
const r = cropRectLoader.item;
|
||||||
if (!r)
|
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 {
|
function updateCrop(mx, my): void {
|
||||||
if (!cropRectLoader.item)
|
if (!cropRectLoader.item)
|
||||||
return;
|
return;
|
||||||
@@ -439,11 +432,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
|
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
updateCrop(mouse.x, mouse.y);
|
updateCrop(mouse.x, mouse.y);
|
||||||
|
|||||||
Reference in New Issue
Block a user