1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32: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
# 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
# - cava for audio visualizer, without it music will just randomly visualize
# - cliphist for clipboard history
# - matugen for dynamic themes based on wallpaper
# - ddcutil for brightness changing
# - qt6-5compat - havent been able to figure out circular cropping pictures without it.
```
2. Configure SwayBG (Optional)

View File

@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
@@ -130,81 +130,92 @@ PanelWindow {
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingL
// Profile Picture Container with circular outline
Rectangle {
// Profile Picture Container
Item {
id: avatarContainer
width: 64
height: 64
radius: width / 2
color: "transparent"
border.color: Qt.rgba(0, 0, 0, 0.15)
border.width: 1
// Hidden image for OpacityMask source
Image {
id: profileImage
property bool hasImage: profileImageLoader.status === Image.Ready
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
anchors.margins: 5 // Inset by border width
fillMode: Image.PreserveAspectCrop
radius: width / 2
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
asynchronous: true
mipmap: true
cache: true
sourceSize.width: 128
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
}
visible: false // This item is never shown directly.
}
// OpacityMask inset by 1px to leave border visible
OpacityMask {
anchors.fill: profileImage
visible: Prefs.profileImage !== "" && profileImage.status !== Image.Error
source: profileImage
maskSource: Rectangle {
width: profileImage.width
height: profileImage.height
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: profileImageLoader
maskEnabled: true
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
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
color: "white"
visible: false
color: "black"
antialiasing: true
}
}
// Fallback background for no image
// Fallback for when there is no image.
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: width / 2
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
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
// Error icon for when the image fails to load.
Text {
anchors.centerIn: parent
text: "warning"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
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
// Circular profile image preview
Rectangle {
Item {
id: avatarContainer
width: 54
height: 54
radius: width / 2
color: Theme.primary
clip: true
property bool hasImage: profileImageInput.text !== "" && avatarImage.status !== Image.Error
Image {
id: avatarImage
property bool hasImage: avatarImageSource.status === Image.Ready
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
anchors.margins: 1
fillMode: Image.PreserveAspectCrop
source: profileImageInput.text.startsWith("/")
? "file://" + profileImageInput.text
: profileImageInput.text
radius: width / 2
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: avatarImageSource
source: {
if (profileImageInput.text === "") return ""
if (profileImageInput.text.startsWith("/")) {
return "file://" + profileImageInput.text
}
return profileImageInput.text
}
smooth: true
asynchronous: true
mipmap: true
cache: true
sourceSize.width: 128
sourceSize.height: 128
visible: false // This item is never shown directly.
}
MultiEffect {
anchors.fill: parent
anchors.margins: 5
source: avatarImageSource
maskEnabled: true
maskSource: settingsCircularMask
visible: avatarContainer.hasImage
property string lastLoggedSource: ""
onStatusChanged: {
if (source !== lastLoggedSource && (status === Image.Ready || status === Image.Error)) {
lastLoggedSource = source
if (status === Image.Ready) {
console.log("Profile image loaded successfully, size:", sourceSize.width + "x" + sourceSize.height)
} else if (status === Image.Error) {
console.log("Profile image failed to load:", source)
}
}
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
}
Item {
id: settingsCircularMask
width: 54 - 10
height: 54 - 10
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
// Fallback icon
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
// Fallback for when there is no image.
Rectangle {
anchors.fill: parent
radius: width / 2
border.color: Qt.rgba(0, 0, 0, 0.15)
border.width: 1
color: "transparent"
visible: avatarContainer.hasImage
color: Theme.primary
visible: !parent.hasImage
Text {
anchors.centerIn: parent
text: "person"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
}
Rectangle {
anchors.fill: parent
anchors.margins: -1
radius: width / 2 + 1
border.color: Qt.rgba(255, 255, 255, 0.1)
border.width: 1
color: "transparent"
visible: avatarContainer.hasImage
// Error icon for when the image fails to load.
Text {
anchors.centerIn: parent
text: "warning"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error
}
}