1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-14 17:52:10 -04: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
property bool hasImage: profileImageLoader.status === Image.Ready
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
radius: width / 2 radius: width / 2
color: "transparent" color: "transparent"
border.color: Qt.rgba(0, 0, 0, 0.15) border.color: Theme.primary
border.width: 1 border.width: 1 // The ring is 1px thick.
visible: parent.hasImage
}
// Hidden image for OpacityMask source // Hidden Image loader. Its only purpose is to load the texture.
Image { Image {
id: profileImage id: profileImageLoader
anchors.fill: parent source: {
anchors.margins: 5 // Inset by border width if (Prefs.profileImage === "") return ""
fillMode: Image.PreserveAspectCrop 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
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
}
maskSource: Rectangle { Item {
width: profileImage.width id: circularMask
height: profileImage.height width: 64 - 10
radius: width / 2 height: 64 - 10
color: "white" layer.enabled: true
layer.smooth: true
visible: false visible: false
}
}
// Fallback background for no image
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 2
radius: width / 2 radius: width / 2
color: Theme.primary color: "black"
visible: Prefs.profileImage === "" antialiasing: true
}
} }
// Fallback icon for no profile picture // Fallback for when there is no image.
Rectangle {
anchors.fill: parent
radius: width / 2
color: Theme.primary
visible: !parent.hasImage
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "person" text: "person"
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 === "" }
} }
// Error icon when image fails to load // Error icon for when the 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
clip: true
property bool hasImage: profileImageInput.text !== "" && avatarImage.status !== Image.Error property bool hasImage: avatarImageSource.status === Image.Ready
Image { // This rectangle provides the themed ring via its border.
id: avatarImage Rectangle {
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)) {
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)
}
}
}
} }
// Fallback icon Item {
Text { id: settingsCircularMask
anchors.centerIn: parent width: 54 - 10
text: profileImageInput.text === "" ? "person" : "warning" height: 54 - 10
font.family: Theme.iconFont layer.enabled: true
font.pixelSize: Theme.iconSize + 8 layer.smooth: true
color: Theme.primaryText visible: false
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: "black"
border.width: 1 antialiasing: true
color: "transparent"
visible: avatarContainer.hasImage
} }
}
// Fallback for when there is no image.
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: -1 radius: width / 2
radius: width / 2 + 1 color: Theme.primary
border.color: Qt.rgba(255, 255, 255, 0.1) visible: !parent.hasImage
border.width: 1
color: "transparent" Text {
visible: avatarContainer.hasImage anchors.centerIn: parent
text: "person"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
}
// 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
} }
} }