NImageRounded: back to using a custom shader as it looks much better than ClippingRectangle.

It seems ClippingRectangle has issues with fractional pixes.
This commit is contained in:
ItsLemmy
2025-11-30 11:46:18 -05:00
parent a773300469
commit 925bbe7a5e
5 changed files with 116 additions and 121 deletions

View File

@@ -32,7 +32,7 @@ NBox {
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage)
fallbackIcon: "person"
borderColor: Color.mPrimary
borderWidth: Style.borderM
borderWidth: Style.borderS * 1.5
}
ColumnLayout {

View File

@@ -63,101 +63,28 @@ ColumnLayout {
}
}
// Large preview with rounded corners and shadow effect
// Large preview area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 180
color: Color.mSurfaceVariant
radius: Style.radiusL
border.color: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
border.width: selectedWallpaper !== "" ? 2 : 1
clip: true
// Mirror WallpaperPanel approach with rounded shader mask
NImageCached {
id: previewCached
// Image with rounded corners
NImageRounded {
anchors.fill: parent
anchors.margins: 4
cacheFolder: Settings.cacheDirImagesWallpapers
visible: selectedWallpaper !== ""
imagePath: selectedWallpaper !== "" ? "file://" + selectedWallpaper : ""
visible: false // used as texture source for the shader
}
ShaderEffect {
anchors.fill: parent
anchors.margins: 4
property var source: ShaderEffectSource {
sourceItem: previewCached
hideSource: true
live: true
recursive: false
format: ShaderEffectSource.RGBA
}
property real itemWidth: width
property real itemHeight: height
property real cornerRadius: Style.radiusL
property real imageOpacity: 1.0
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
supportsAtlasTextures: false
blending: true
}
// Loading placeholder
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
radius: Style.radiusL
visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
NIcon {
icon: "image"
pointSize: Style.fontSizeXXL
color: Color.mOnSurfaceVariant
anchors.centerIn: parent
}
}
// Error placeholder
Rectangle {
anchors.fill: parent
color: Color.mError
opacity: 0.1
radius: Style.radiusL
visible: previewCached.status === Image.Error && selectedWallpaper !== ""
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginS
NIcon {
icon: "alert-circle"
pointSize: Style.fontSizeXXL
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("setup.wallpaper.preview-error")
pointSize: Style.fontSizeS
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
}
}
NBusyIndicator {
anchors.centerIn: parent
visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
running: visible
size: 28
borderColor: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
borderWidth: selectedWallpaper !== "" ? 2 : 1
}
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginL
visible: selectedWallpaper === ""
opacity: 0.6
Rectangle {
Layout.alignment: Qt.AlignHCenter
@@ -165,12 +92,11 @@ ColumnLayout {
height: 64
radius: width / 2
color: Color.mPrimary
opacity: 0.15
NIcon {
icon: "sparkles"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
color: Color.mOnPrimary
anchors.centerIn: parent
}
}
@@ -232,19 +158,22 @@ ColumnLayout {
Repeater {
model: filteredWallpapers
delegate: Rectangle {
delegate: Item {
Layout.preferredWidth: 120
Layout.preferredHeight: 80
color: Color.mSurface
border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
border.width: selectedWallpaper === modelData ? 2 : 1
clip: true
Rectangle {
anchors.fill: parent
color: Color.transparent
border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
border.width: selectedWallpaper === modelData ? 2 : 1
}
// Cached thumbnail
NImageCached {
id: thumbCached
anchors.fill: parent
anchors.margins: 3
anchors.margins: selectedWallpaper === modelData ? 2 : 1
source: "file://" + modelData
}

View File

@@ -11,8 +11,11 @@ layout(std140, binding = 0) uniform buf {
// Custom properties with non-conflicting names
float itemWidth;
float itemHeight;
float sourceWidth;
float sourceHeight;
float cornerRadius;
float imageOpacity;
int fillMode;
} ubuf;
// Function to calculate the signed distance from a point to a rounded box
@@ -24,33 +27,75 @@ float roundedBoxSDF(vec2 centerPos, vec2 boxSize, float radius) {
void main() {
// Get size from uniforms
vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight);
vec2 sourceSize = vec2(ubuf.sourceWidth, ubuf.sourceHeight);
float cornerRadius = ubuf.cornerRadius;
float itemOpacity = ubuf.imageOpacity;
int fillMode = ubuf.fillMode;
// Normalize coordinates to [-0.5, 0.5] range
vec2 uv = qt_TexCoord0 - 0.5;
// Work in pixel space for accurate rounded rectangle calculation
vec2 pixelPos = qt_TexCoord0 * itemSize;
// Scale by aspect ratio to maintain uniform rounding
vec2 aspectRatio = itemSize / max(itemSize.x, itemSize.y);
uv *= aspectRatio;
// Calculate distance to rounded rectangle edge (in pixels)
vec2 centerOffset = pixelPos - itemSize * 0.5;
float distance = roundedBoxSDF(centerOffset, itemSize * 0.5, cornerRadius);
// Calculate half size in normalized space
vec2 halfSize = 0.5 * aspectRatio;
// Create smooth alpha mask for edge with anti-aliasing
float alpha = 1.0 - smoothstep(-0.5, 0.5, distance);
// Normalize the corner radius
float normalizedRadius = cornerRadius / max(itemSize.x, itemSize.y);
// Calculate UV coordinates based on fill mode
vec2 imageUV = qt_TexCoord0;
// Calculate distance to rounded rectangle
float distance = roundedBoxSDF(uv, halfSize, normalizedRadius);
// fillMode constants from Qt:
// Image.Stretch = 0
// Image.PreserveAspectFit = 1
// Image.PreserveAspectCrop = 2
// Image.Tile = 3
// Image.TileVertically = 4
// Image.TileHorizontally = 5
// Image.Pad = 6
// Create smooth alpha mask
float smoothedAlpha = 1.0 - smoothstep(0.0, fwidth(distance), distance);
if (fillMode == 2) { // PreserveAspectCrop
// Calculate aspect ratios
float itemAspect = itemSize.x / itemSize.y;
float sourceAspect = sourceSize.x / sourceSize.y;
// Calculate the scale needed to cover the item area
vec2 scale;
if (sourceAspect > itemAspect) {
// Image is wider - fit height, crop sides
scale.y = 1.0;
scale.x = sourceAspect / itemAspect;
} else {
// Image is taller - fit width, crop top/bottom
scale.x = 1.0;
scale.y = itemAspect / sourceAspect;
}
// Apply scale and center
imageUV = (qt_TexCoord0 - 0.5) / scale + 0.5;
} else if (fillMode == 1) { // PreserveAspectFit
float itemAspect = itemSize.x / itemSize.y;
float sourceAspect = sourceSize.x / sourceSize.y;
vec2 scale;
if (sourceAspect > itemAspect) {
// Image is wider - fit width, letterbox top/bottom
scale.x = 1.0;
scale.y = itemAspect / sourceAspect;
} else {
// Image is taller - fit height, letterbox sides
scale.y = 1.0;
scale.x = sourceAspect / itemAspect;
}
imageUV = (qt_TexCoord0 - 0.5) * scale + 0.5;
}
// For Stretch (0) or other modes, use qt_TexCoord0 as-is
// Sample the texture
vec4 color = texture(source, qt_TexCoord0);
vec4 color = texture(source, imageUV);
// Apply the rounded mask and opacity
// Make sure areas outside the rounded rect are completely transparent
float finalAlpha = color.a * smoothedAlpha * itemOpacity * ubuf.qt_Opacity;
float finalAlpha = color.a * alpha * itemOpacity * ubuf.qt_Opacity;
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
}

Binary file not shown.

View File

@@ -19,17 +19,19 @@ Item {
signal statusChanged(int status)
ClippingRectangle {
Rectangle {
anchors.fill: parent
color: Color.transparent
radius: root.radius
border.color: root.borderColor
color: Color.transparent
border.width: root.borderWidth
border.color: root.borderColor
Image {
id: imageSource
anchors.fill: parent
visible: !showFallback
source: imagePath
anchors.margins: root.borderWidth
visible: false
source: root.imagePath
mipmap: true
smooth: true
asynchronous: true
@@ -38,11 +40,30 @@ Item {
onStatusChanged: root.statusChanged(status)
}
ShaderEffect {
anchors.fill: parent
anchors.margins: root.borderWidth
visible: !root.showFallback
property variant source: imageSource
property real itemWidth: width
property real itemHeight: height
property real sourceWidth: imageSource.sourceSize.width
property real sourceHeight: imageSource.sourceSize.height
property real cornerRadius: Math.max(0, root.radius - root.borderWidth)
property real imageOpacity: 1.0
property int fillMode: root.imageFillMode
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
supportsAtlasTextures: false
blending: true
}
NIcon {
anchors.centerIn: parent
visible: showFallback
icon: fallbackIcon
pointSize: fallbackIconSize
anchors.fill: parent
anchors.margins: root.borderWidth
visible: root.showFallback
icon: root.fallbackIcon
pointSize: root.fallbackIconSize
}
}
}