diff --git a/Modules/Panels/ControlCenter/Cards/ProfileCard.qml b/Modules/Panels/ControlCenter/Cards/ProfileCard.qml index f827b506..62ca83e1 100644 --- a/Modules/Panels/ControlCenter/Cards/ProfileCard.qml +++ b/Modules/Panels/ControlCenter/Cards/ProfileCard.qml @@ -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 { diff --git a/Modules/Panels/SetupWizard/SetupWallpaperStep.qml b/Modules/Panels/SetupWizard/SetupWallpaperStep.qml index e453bc7d..c5938b6c 100644 --- a/Modules/Panels/SetupWizard/SetupWallpaperStep.qml +++ b/Modules/Panels/SetupWizard/SetupWallpaperStep.qml @@ -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 } diff --git a/Shaders/frag/rounded_image.frag b/Shaders/frag/rounded_image.frag index 9d493b21..e38c0a00 100644 --- a/Shaders/frag/rounded_image.frag +++ b/Shaders/frag/rounded_image.frag @@ -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; - - // Normalize coordinates to [-0.5, 0.5] range - vec2 uv = qt_TexCoord0 - 0.5; - - // Scale by aspect ratio to maintain uniform rounding - vec2 aspectRatio = itemSize / max(itemSize.x, itemSize.y); - uv *= aspectRatio; - - // Calculate half size in normalized space - vec2 halfSize = 0.5 * aspectRatio; - - // Normalize the corner radius - float normalizedRadius = cornerRadius / max(itemSize.x, itemSize.y); - - // Calculate distance to rounded rectangle - float distance = roundedBoxSDF(uv, halfSize, normalizedRadius); - - // Create smooth alpha mask - float smoothedAlpha = 1.0 - smoothstep(0.0, fwidth(distance), distance); - + int fillMode = ubuf.fillMode; + + // Work in pixel space for accurate rounded rectangle calculation + vec2 pixelPos = qt_TexCoord0 * itemSize; + + // Calculate distance to rounded rectangle edge (in pixels) + vec2 centerOffset = pixelPos - itemSize * 0.5; + float distance = roundedBoxSDF(centerOffset, itemSize * 0.5, cornerRadius); + + // Create smooth alpha mask for edge with anti-aliasing + float alpha = 1.0 - smoothstep(-0.5, 0.5, distance); + + // Calculate UV coordinates based on fill mode + vec2 imageUV = qt_TexCoord0; + + // 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 + + 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); } \ No newline at end of file diff --git a/Shaders/qsb/rounded_image.frag.qsb b/Shaders/qsb/rounded_image.frag.qsb index 666f6434..a82f6550 100644 Binary files a/Shaders/qsb/rounded_image.frag.qsb and b/Shaders/qsb/rounded_image.frag.qsb differ diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index f93ea5a3..a403c1d6 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -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 } } }