mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-05 22:26:16 +00:00
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:
@@ -32,7 +32,7 @@ NBox {
|
|||||||
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage)
|
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage)
|
||||||
fallbackIcon: "person"
|
fallbackIcon: "person"
|
||||||
borderColor: Color.mPrimary
|
borderColor: Color.mPrimary
|
||||||
borderWidth: Style.borderM
|
borderWidth: Style.borderS * 1.5
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|||||||
@@ -63,101 +63,28 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Large preview with rounded corners and shadow effect
|
// Large preview area
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.minimumHeight: 180
|
Layout.minimumHeight: 180
|
||||||
color: Color.mSurfaceVariant
|
color: Color.mSurfaceVariant
|
||||||
radius: Style.radiusL
|
radius: Style.radiusL
|
||||||
border.color: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
|
|
||||||
border.width: selectedWallpaper !== "" ? 2 : 1
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
// Mirror WallpaperPanel approach with rounded shader mask
|
// Image with rounded corners
|
||||||
NImageCached {
|
NImageRounded {
|
||||||
id: previewCached
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 4
|
visible: selectedWallpaper !== ""
|
||||||
cacheFolder: Settings.cacheDirImagesWallpapers
|
|
||||||
imagePath: selectedWallpaper !== "" ? "file://" + 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
|
radius: Style.radiusL
|
||||||
visible: (previewCached.status === Image.Loading || previewCached.status === Image.Null) && selectedWallpaper !== ""
|
borderColor: selectedWallpaper !== "" ? Color.mPrimary : Color.mOutline
|
||||||
|
borderWidth: selectedWallpaper !== "" ? 2 : 1
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Style.marginL
|
spacing: Style.marginL
|
||||||
visible: selectedWallpaper === ""
|
visible: selectedWallpaper === ""
|
||||||
opacity: 0.6
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
@@ -165,12 +92,11 @@ ColumnLayout {
|
|||||||
height: 64
|
height: 64
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
opacity: 0.15
|
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
icon: "sparkles"
|
icon: "sparkles"
|
||||||
pointSize: Style.fontSizeXXL
|
pointSize: Style.fontSizeXXL
|
||||||
color: Color.mPrimary
|
color: Color.mOnPrimary
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,19 +158,22 @@ ColumnLayout {
|
|||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: filteredWallpapers
|
model: filteredWallpapers
|
||||||
delegate: Rectangle {
|
delegate: Item {
|
||||||
Layout.preferredWidth: 120
|
Layout.preferredWidth: 120
|
||||||
Layout.preferredHeight: 80
|
Layout.preferredHeight: 80
|
||||||
color: Color.mSurface
|
|
||||||
border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
|
Rectangle {
|
||||||
border.width: selectedWallpaper === modelData ? 2 : 1
|
anchors.fill: parent
|
||||||
clip: true
|
color: Color.transparent
|
||||||
|
border.color: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline
|
||||||
|
border.width: selectedWallpaper === modelData ? 2 : 1
|
||||||
|
}
|
||||||
|
|
||||||
// Cached thumbnail
|
// Cached thumbnail
|
||||||
NImageCached {
|
NImageCached {
|
||||||
id: thumbCached
|
id: thumbCached
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 3
|
anchors.margins: selectedWallpaper === modelData ? 2 : 1
|
||||||
source: "file://" + modelData
|
source: "file://" + modelData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ layout(std140, binding = 0) uniform buf {
|
|||||||
// Custom properties with non-conflicting names
|
// Custom properties with non-conflicting names
|
||||||
float itemWidth;
|
float itemWidth;
|
||||||
float itemHeight;
|
float itemHeight;
|
||||||
|
float sourceWidth;
|
||||||
|
float sourceHeight;
|
||||||
float cornerRadius;
|
float cornerRadius;
|
||||||
float imageOpacity;
|
float imageOpacity;
|
||||||
|
int fillMode;
|
||||||
} ubuf;
|
} ubuf;
|
||||||
|
|
||||||
// Function to calculate the signed distance from a point to a rounded box
|
// 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() {
|
void main() {
|
||||||
// Get size from uniforms
|
// Get size from uniforms
|
||||||
vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight);
|
vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight);
|
||||||
|
vec2 sourceSize = vec2(ubuf.sourceWidth, ubuf.sourceHeight);
|
||||||
float cornerRadius = ubuf.cornerRadius;
|
float cornerRadius = ubuf.cornerRadius;
|
||||||
float itemOpacity = ubuf.imageOpacity;
|
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);
|
// Calculate distance to rounded rectangle edge (in pixels)
|
||||||
uv *= aspectRatio;
|
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
|
||||||
// Create smooth alpha mask
|
// Image.PreserveAspectFit = 1
|
||||||
float smoothedAlpha = 1.0 - smoothstep(0.0, fwidth(distance), distance);
|
// Image.PreserveAspectCrop = 2
|
||||||
|
// Image.Tile = 3
|
||||||
|
// Image.TileVertically = 4
|
||||||
|
// Image.TileHorizontally = 5
|
||||||
|
// Image.Pad = 6
|
||||||
|
|
||||||
|
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
|
// Sample the texture
|
||||||
vec4 color = texture(source, qt_TexCoord0);
|
vec4 color = texture(source, imageUV);
|
||||||
|
|
||||||
// Apply the rounded mask and opacity
|
// Apply the rounded mask and opacity
|
||||||
// Make sure areas outside the rounded rect are completely transparent
|
float finalAlpha = color.a * alpha * itemOpacity * ubuf.qt_Opacity;
|
||||||
float finalAlpha = color.a * smoothedAlpha * itemOpacity * ubuf.qt_Opacity;
|
|
||||||
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
|
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -19,17 +19,19 @@ Item {
|
|||||||
|
|
||||||
signal statusChanged(int status)
|
signal statusChanged(int status)
|
||||||
|
|
||||||
ClippingRectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Color.transparent
|
|
||||||
radius: root.radius
|
radius: root.radius
|
||||||
border.color: root.borderColor
|
color: Color.transparent
|
||||||
border.width: root.borderWidth
|
border.width: root.borderWidth
|
||||||
|
border.color: root.borderColor
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
id: imageSource
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !showFallback
|
anchors.margins: root.borderWidth
|
||||||
source: imagePath
|
visible: false
|
||||||
|
source: root.imagePath
|
||||||
mipmap: true
|
mipmap: true
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
@@ -38,11 +40,30 @@ Item {
|
|||||||
onStatusChanged: root.statusChanged(status)
|
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 {
|
NIcon {
|
||||||
anchors.centerIn: parent
|
anchors.fill: parent
|
||||||
visible: showFallback
|
anchors.margins: root.borderWidth
|
||||||
icon: fallbackIcon
|
visible: root.showFallback
|
||||||
pointSize: fallbackIconSize
|
icon: root.fallbackIcon
|
||||||
|
pointSize: root.fallbackIconSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user