1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

MultiEffect for rounded icons

This commit is contained in:
bbedward
2025-07-15 21:31:41 -04:00
parent b2b664f581
commit 994be601ae
3 changed files with 136 additions and 108 deletions

View File

@@ -12,14 +12,13 @@ Specifically created for [Niri](https://github.com/YaLTeR/niri).
```bash ```bash
# Arch # Arch
paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard ddcutil qt6-5compat paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard ddcutil
# Some dependencies are optional # Some dependencies are optional
# - cava for audio visualizer, without it music will just randomly visualize # - cava for audio visualizer, without it music will just randomly visualize
# - cliphist for clipboard history # - cliphist for clipboard history
# - matugen for dynamic themes based on wallpaper # - matugen for dynamic themes based on wallpaper
# - ddcutil for brightness changing # - ddcutil for brightness changing
# - qt6-5compat - havent been able to figure out circular cropping pictures without it.
``` ```
2. Configure SwayBG (Optional) 2. Configure SwayBG (Optional)

View File

@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Qt5Compat.GraphicalEffects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
@@ -130,81 +130,92 @@ PanelWindow {
anchors.rightMargin: Theme.spacingL anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingL spacing: Theme.spacingL
// Profile Picture Container with circular outline // Profile Picture Container
Rectangle { Item {
id: avatarContainer
width: 64 width: 64
height: 64 height: 64
radius: width / 2
color: "transparent" property bool hasImage: profileImageLoader.status === Image.Ready
border.color: Qt.rgba(0, 0, 0, 0.15)
border.width: 1 // This rectangle provides the themed ring via its border.
Rectangle {
// Hidden image for OpacityMask source
Image {
id: profileImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 5 // Inset by border width radius: width / 2
fillMode: Image.PreserveAspectCrop color: "transparent"
border.color: Theme.primary
border.width: 1 // The ring is 1px thick.
visible: parent.hasImage
}
// Hidden Image loader. Its only purpose is to load the texture.
Image {
id: profileImageLoader
source: {
if (Prefs.profileImage === "") return ""
if (Prefs.profileImage.startsWith("/")) {
return "file://" + Prefs.profileImage
}
return Prefs.profileImage
}
smooth: true smooth: true
asynchronous: true asynchronous: true
mipmap: true mipmap: true
cache: true cache: true
sourceSize.width: 128 visible: false // This item is never shown directly.
sourceSize.height: 128
visible: false // Hidden, only used as mask source
source: {
if (Prefs.profileImage === "") return ""
// Add file:// prefix if it's a local path (starts with /)
if (Prefs.profileImage.startsWith("/")) {
return "file://" + Prefs.profileImage
}
// Return as-is if it already has a protocol or is a web URL
return Prefs.profileImage
}
} }
// OpacityMask inset by 1px to leave border visible MultiEffect {
OpacityMask { anchors.fill: parent
anchors.fill: profileImage anchors.margins: 5
visible: Prefs.profileImage !== "" && profileImage.status !== Image.Error source: profileImageLoader
source: profileImage maskEnabled: true
maskSource: circularMask
maskSource: Rectangle { visible: avatarContainer.hasImage
width: profileImage.width maskThresholdMin: 0.5
height: profileImage.height maskSpreadAtMin: 1.0
}
Item {
id: circularMask
width: 64 - 10
height: 64 - 10
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2 radius: width / 2
color: "white" color: "black"
visible: false antialiasing: true
} }
} }
// Fallback background for no image // Fallback for when there is no image.
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 2
radius: width / 2 radius: width / 2
color: Theme.primary color: Theme.primary
visible: Prefs.profileImage === "" visible: !parent.hasImage
Text {
anchors.centerIn: parent
text: "person"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
} }
// Fallback icon for no profile picture // Error icon for when the image fails to load.
Text {
anchors.centerIn: parent
text: "person"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
visible: Prefs.profileImage === ""
}
// Error icon when image fails to load
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "warning" text: "warning"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8 font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText color: Theme.primaryText
visible: Prefs.profileImage !== "" && profileImage.status === Image.Error visible: Prefs.profileImage !== "" && profileImageLoader.status === Image.Error
} }
} }

View File

@@ -166,73 +166,91 @@ PanelWindow {
spacing: Theme.spacingM spacing: Theme.spacingM
// Circular profile image preview // Circular profile image preview
Rectangle { Item {
id: avatarContainer id: avatarContainer
width: 54 width: 54
height: 54 height: 54
radius: width / 2
color: Theme.primary property bool hasImage: avatarImageSource.status === Image.Ready
clip: true
// This rectangle provides the themed ring via its border.
property bool hasImage: profileImageInput.text !== "" && avatarImage.status !== Image.Error Rectangle {
Image {
id: avatarImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 radius: width / 2
fillMode: Image.PreserveAspectCrop color: "transparent"
source: profileImageInput.text.startsWith("/") border.color: Theme.primary
? "file://" + profileImageInput.text border.width: 1 // The ring is 1px thick.
: profileImageInput.text visible: parent.hasImage
}
// Hidden Image loader. Its only purpose is to load the texture.
Image {
id: avatarImageSource
source: {
if (profileImageInput.text === "") return ""
if (profileImageInput.text.startsWith("/")) {
return "file://" + profileImageInput.text
}
return profileImageInput.text
}
smooth: true smooth: true
asynchronous: true asynchronous: true
mipmap: true mipmap: true
cache: true cache: true
sourceSize.width: 128 visible: false // This item is never shown directly.
sourceSize.height: 128 }
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: avatarImageSource
maskEnabled: true
maskSource: settingsCircularMask
visible: avatarContainer.hasImage visible: avatarContainer.hasImage
maskThresholdMin: 0.5
property string lastLoggedSource: "" maskSpreadAtMin: 1.0
}
onStatusChanged: {
if (source !== lastLoggedSource && (status === Image.Ready || status === Image.Error)) { Item {
lastLoggedSource = source id: settingsCircularMask
if (status === Image.Ready) { width: 54 - 10
console.log("Profile image loaded successfully, size:", sourceSize.width + "x" + sourceSize.height) height: 54 - 10
} else if (status === Image.Error) { layer.enabled: true
console.log("Profile image failed to load:", source) layer.smooth: true
} visible: false
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
} }
} }
// Fallback icon // Fallback for when there is no image.
Text {
anchors.centerIn: parent
text: profileImageInput.text === "" ? "person" : "warning"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
visible: !avatarContainer.hasImage
}
// Decorative rings
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: width / 2 radius: width / 2
border.color: Qt.rgba(0, 0, 0, 0.15) color: Theme.primary
border.width: 1 visible: !parent.hasImage
color: "transparent"
visible: avatarContainer.hasImage Text {
anchors.centerIn: parent
text: "person"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
} }
Rectangle {
anchors.fill: parent // Error icon for when the image fails to load.
anchors.margins: -1 Text {
radius: width / 2 + 1 anchors.centerIn: parent
border.color: Qt.rgba(255, 255, 255, 0.1) text: "warning"
border.width: 1 font.family: Theme.iconFont
color: "transparent" font.pixelSize: Theme.iconSize + 8
visible: avatarContainer.hasImage color: Theme.primaryText
visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error
} }
} }