feat: wallpaper cropper sdf shader rectangle, better confirmation controls + hide passive tray icons
C++ / fmt (pull_request) Successful in 4s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 32s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m46s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 49s
C++ / fmt (pull_request) Successful in 4s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 32s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m46s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 49s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,37 +205,68 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: wrapper.buttonRowHeight + Tokens.spacing.medium
|
||||
// anchors.top: screenSelector.bottom
|
||||
|
||||
CustomClippingRect {
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
|
||||
opacity: scaledImg.status === Image.Ready ? 0 : 1
|
||||
active: opacity > 0
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
|
||||
|
||||
color: Colors.palette.m3primaryContainer
|
||||
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 {
|
||||
id: scaledImg
|
||||
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
retainWhileLoading: true
|
||||
fillMode: Image.Stretch
|
||||
source: Wallpapers.current
|
||||
sourceSize.height: parent.height
|
||||
sourceSize.width: parent.width
|
||||
|
||||
onPaintedWidthChanged: {
|
||||
if (paintedWidth > 0 && cropRectLoader.item) {
|
||||
if (!wrapper.screens.length)
|
||||
wrapper.refreshScreens();
|
||||
|
||||
wrapper.syncCropToScreen();
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
}
|
||||
onSourceChanged: {
|
||||
if (cropRectLoader.item) {
|
||||
if (!wrapper.screens.length)
|
||||
wrapper.refreshScreens();
|
||||
retainWhileLoading: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
wrapper.syncCropToScreen();
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
onStatusChanged: {
|
||||
if (scaledImg.status == Image.Ready && cropRectLoader.item) {
|
||||
if (!wrapper.screens.length)
|
||||
wrapper.refreshScreens();
|
||||
|
||||
wrapper.syncCropToScreen();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -239,30 +280,34 @@ 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 = scaledImg.paintedHeight;
|
||||
let fittedHeight = imageH;
|
||||
let fittedWidth = fittedHeight * aspectRatio;
|
||||
|
||||
if (fittedWidth > scaledImg.paintedWidth) {
|
||||
fittedWidth = scaledImg.paintedWidth;
|
||||
if (fittedWidth > imageW) {
|
||||
fittedWidth = imageW;
|
||||
fittedHeight = fittedWidth / aspectRatio;
|
||||
}
|
||||
|
||||
return fittedWidth;
|
||||
}
|
||||
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
|
||||
|
||||
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 = imageX + (scaledImg.paintedWidth - width) / 2;
|
||||
y = imageY + (scaledImg.paintedHeight - height) / 2;
|
||||
x = Math.round(imageX + (imageW - width) / 2);
|
||||
y = Math.round(imageY + (imageH - height) / 2);
|
||||
}
|
||||
|
||||
function clampToBounds() {
|
||||
x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width));
|
||||
y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height));
|
||||
x = Math.max(imageX, Math.min(x, imageX + imageW - width));
|
||||
y = Math.max(imageY, Math.min(y, imageY + imageH - height));
|
||||
}
|
||||
|
||||
function restoreFromData() {
|
||||
@@ -270,8 +315,8 @@ Item {
|
||||
|
||||
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);
|
||||
x = Math.round(imageX + (data.x * imageW));
|
||||
y = Math.round(imageY + (data.y * imageH));
|
||||
|
||||
clampToBounds();
|
||||
} else {
|
||||
@@ -280,15 +325,12 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
border.color: Colors.palette.m3primary
|
||||
border.width: 2
|
||||
height: baseHeight / zoom
|
||||
opacity: 1
|
||||
width: baseWidth / zoom
|
||||
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 opacity {
|
||||
Anim {
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -300,20 +342,66 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
function updateCrop(mouseX, mouseY) {
|
||||
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 = mouseX - cropRect.width * 0.5;
|
||||
let ny = mouseY - cropRect.height * 0.5;
|
||||
let nx = mx - grabX;
|
||||
let ny = my - grabY;
|
||||
|
||||
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));
|
||||
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;
|
||||
@@ -323,6 +411,8 @@ Item {
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed) {
|
||||
updateCrop(mouse.x, mouse.y);
|
||||
@@ -330,11 +420,9 @@ Item {
|
||||
}
|
||||
}
|
||||
onPressed: mouse => {
|
||||
beginGrab(mouse.x, mouse.y);
|
||||
updateCrop(mouse.x, mouse.y);
|
||||
wrapper.changesMade = true;
|
||||
}
|
||||
onReleased: {
|
||||
wrapper.changesMade = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Binary file not shown.
@@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user