1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 03:28:28 -04:00

qs/media: replace FBOs with ClippingRectangle and rewrite seekbar as a

shader
This commit is contained in:
bbedward
2026-07-07 18:16:26 -04:00
parent 0511cd19df
commit ee6f7b4798
17 changed files with 427 additions and 578 deletions
+5 -31
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -591,24 +591,11 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
Rectangle { ClippingRectangle {
id: gridProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
visible: false color: "transparent"
layer.enabled: true
}
Item {
anchors.fill: parent
visible: gridButtonRect.isHolding visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
@@ -729,24 +716,11 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
Rectangle { ClippingRectangle {
id: listProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
visible: false color: "transparent"
layer.enabled: true
}
Item {
anchors.fill: parent
visible: listButtonRect.isHolding visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
@@ -240,8 +240,10 @@ DankPopout {
Connections { Connections {
target: root target: root
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) if (!root.shouldBeVisible)
mainContainer.forceActiveFocus(); return;
mainContainer.forceActiveFocus();
tabBar.snapIndicator();
} }
} }
+24 -11
View File
@@ -25,6 +25,11 @@ Item {
property string section: "" property string section: ""
property int barPosition: SettingsData.Position.Top property int barPosition: SettingsData.Position.Top
readonly property color accent: MediaAccentService.accent
readonly property color onAccent: MediaAccentService.onAccent
readonly property color accentHover: MediaAccentService.accentHover
readonly property color accentPressed: MediaAccentService.accentPressed
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
@@ -298,8 +303,16 @@ Item {
Component.onCompleted: syncArt() Component.onCompleted: syncArt()
function syncArt() { function syncArt() {
if (curArt === "" || layerA.art == curArt || layerB.art == curArt) if (curArt === "")
return; return;
const frontArt = _showA ? layerA.art : layerB.art;
const backArt = _showA ? layerB.art : layerA.art;
if (frontArt == curArt)
return;
if (backArt == curArt) {
_showA = !_showA;
return;
}
if (_showA) if (_showA)
layerB.art = curArt; layerB.art = curArt;
else else
@@ -564,13 +577,13 @@ Item {
height: 40 height: 40
radius: 20 radius: 20
anchors.centerIn: parent anchors.centerIn: parent
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0) color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "shuffle" name: "shuffle"
size: 20 size: 20
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -626,13 +639,13 @@ Item {
height: 50 height: 50
radius: 25 radius: 25
anchors.centerIn: parent anchors.centerIn: parent
color: Theme.primary color: root.accent
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28 size: 28
color: Theme.background color: root.onAccent
weight: 500 weight: 500
} }
@@ -696,7 +709,7 @@ Item {
height: 40 height: 40
radius: 20 radius: 20
anchors.centerIn: parent anchors.centerIn: parent
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0) color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -713,7 +726,7 @@ Item {
} }
} }
size: 20 size: 20
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -752,7 +765,7 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185 y: 185
color: playerSelectorArea.containsMouse || playersExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0) color: playerSelectorArea.containsMouse || playersExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
border.color: Theme.outlineStrong border.color: Theme.outlineStrong
border.width: 1 border.width: 1
z: 100 z: 100
@@ -819,7 +832,7 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130 y: 130
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0) color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
border.width: 1 border.width: 1
z: 101 z: 101
@@ -831,7 +844,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
name: getVolumeIcon() name: getVolumeIcon()
size: 18 size: 18
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5) color: volumeAvailable && currentVolume > 0 ? root.accent : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
} }
MouseArea { MouseArea {
@@ -882,7 +895,7 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240 y: 240
color: audioDevicesArea.containsMouse || devicesExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0) color: audioDevicesArea.containsMouse || devicesExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
border.color: Theme.outlineStrong border.color: Theme.outlineStrong
border.width: 1 border.width: 1
z: 100 z: 100
@@ -154,13 +154,13 @@ Card {
width: 32 width: 32
height: 32 height: 32
radius: 16 radius: 16
color: Theme.primary color: MediaAccentService.accent
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 16 size: 16
color: Theme.background color: MediaAccentService.onAccent
} }
MouseArea { MouseArea {
+5 -31
View File
@@ -1,7 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Effects import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -567,24 +567,11 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
Rectangle { ClippingRectangle {
id: gridProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
visible: false color: "transparent"
layer.enabled: true
}
Item {
anchors.fill: parent
visible: gridButtonRect.isHolding visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
@@ -700,24 +687,11 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
Rectangle { ClippingRectangle {
id: listProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
visible: false color: "transparent"
layer.enabled: true
}
Item {
anchors.fill: parent
visible: listButtonRect.isHolding visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
+5 -21
View File
@@ -4,6 +4,7 @@ import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Widgets
DankOSD { DankOSD {
id: root id: root
@@ -185,10 +186,11 @@ DankOSD {
visible: false visible: false
} }
Item { ClippingRectangle {
id: blurredBg
anchors.fill: parent anchors.fill: parent
visible: false radius: Theme.cornerRadius
color: "transparent"
opacity: 0.7
MultiEffect { MultiEffect {
anchors.centerIn: parent anchors.centerIn: parent
@@ -203,24 +205,6 @@ DankOSD {
} }
} }
Rectangle {
id: bgMask
anchors.fill: parent
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
MultiEffect {
anchors.fill: parent
source: blurredBg
maskEnabled: true
maskSource: bgMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
opacity: 0.7
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
+20 -31
View File
@@ -1,7 +1,7 @@
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -520,28 +520,27 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
Image { ClippingRectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
source: { radius: Theme.cornerRadius - 1
var wp = Theme.wallpaperPath; color: "transparent"
if (!wp || wp === "" || wp.startsWith("#"))
return ""; Image {
if (wp.startsWith("file://")) anchors.fill: parent
wp = wp.substring(7); source: {
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); var wp = Theme.wallpaperPath;
} if (!wp || wp === "" || wp.startsWith("#"))
fillMode: Image.PreserveAspectCrop return "";
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#") if (wp.startsWith("file://"))
sourceSize.width: 120 wp = wp.substring(7);
sourceSize.height: 120 return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
asynchronous: true }
layer.enabled: true fillMode: Image.PreserveAspectCrop
layer.effect: MultiEffect { visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
maskEnabled: true sourceSize.width: 120
maskSource: autoWallpaperMask sourceSize.height: 120
maskThresholdMin: 0.5 asynchronous: true
maskSpreadAtMin: 1
} }
} }
@@ -553,16 +552,6 @@ Item {
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
} }
Rectangle {
id: autoWallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette" name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
+64 -97
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -75,28 +75,27 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
Image { ClippingRectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
source: { radius: Theme.cornerRadius - 1
var wp = root.currentWallpaper; color: "transparent"
if (wp === "" || wp.startsWith("#"))
return ""; Image {
if (wp.startsWith("file://")) anchors.fill: parent
wp = wp.substring(7); source: {
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); var wp = root.currentWallpaper;
} if (wp === "" || wp.startsWith("#"))
fillMode: Image.PreserveAspectCrop return "";
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#") if (wp.startsWith("file://"))
sourceSize.width: 160 wp = wp.substring(7);
sourceSize.height: 160 return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
asynchronous: true }
layer.enabled: true fillMode: Image.PreserveAspectCrop
layer.effect: MultiEffect { visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
maskEnabled: true sourceSize.width: 160
maskSource: wallpaperMask sourceSize.height: 160
maskThresholdMin: 0.5 asynchronous: true
maskSpreadAtMin: 1
} }
} }
@@ -108,16 +107,6 @@ Item {
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#") visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
} }
Rectangle {
id: wallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "image" name: "image"
@@ -421,31 +410,30 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
Image { ClippingRectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
source: { radius: Theme.cornerRadius - 1
var wp = SessionData.wallpaperPathLight; color: "transparent"
if (wp === "" || wp.startsWith("#"))
return ""; Image {
if (wp.startsWith("file://")) anchors.fill: parent
wp = wp.substring(7); source: {
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); var wp = SessionData.wallpaperPathLight;
} if (wp === "" || wp.startsWith("#"))
fillMode: Image.PreserveAspectCrop return "";
visible: { if (wp.startsWith("file://"))
var lightWallpaper = SessionData.wallpaperPathLight; wp = wp.substring(7);
return lightWallpaper !== "" && !lightWallpaper.startsWith("#"); return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
} }
sourceSize.width: 160 fillMode: Image.PreserveAspectCrop
sourceSize.height: 160 visible: {
asynchronous: true var lightWallpaper = SessionData.wallpaperPathLight;
layer.enabled: true return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
layer.effect: MultiEffect { }
maskEnabled: true sourceSize.width: 160
maskSource: lightMask sourceSize.height: 160
maskThresholdMin: 0.5 asynchronous: true
maskSpreadAtMin: 1
} }
} }
@@ -463,16 +451,6 @@ Item {
} }
} }
Rectangle {
id: lightMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "light_mode" name: "light_mode"
@@ -611,31 +589,30 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
Image { ClippingRectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
source: { radius: Theme.cornerRadius - 1
var wp = SessionData.wallpaperPathDark; color: "transparent"
if (wp === "" || wp.startsWith("#"))
return ""; Image {
if (wp.startsWith("file://")) anchors.fill: parent
wp = wp.substring(7); source: {
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); var wp = SessionData.wallpaperPathDark;
} if (wp === "" || wp.startsWith("#"))
fillMode: Image.PreserveAspectCrop return "";
visible: { if (wp.startsWith("file://"))
var darkWallpaper = SessionData.wallpaperPathDark; wp = wp.substring(7);
return darkWallpaper !== "" && !darkWallpaper.startsWith("#"); return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
} }
sourceSize.width: 160 fillMode: Image.PreserveAspectCrop
sourceSize.height: 160 visible: {
asynchronous: true var darkWallpaper = SessionData.wallpaperPathDark;
layer.enabled: true return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
layer.effect: MultiEffect { }
maskEnabled: true sourceSize.width: 160
maskSource: darkMask sourceSize.height: 160
maskThresholdMin: 0.5 asynchronous: true
maskSpreadAtMin: 1
} }
} }
@@ -653,16 +630,6 @@ Item {
} }
} }
Rectangle {
id: darkMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "dark_mode" name: "dark_mode"
@@ -1,8 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
Item { Item {
@@ -58,23 +58,6 @@ Item {
visible: intersectsViewport visible: intersectsViewport
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4 opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
Rectangle {
id: maskRect
width: root.width
height: root.height
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: maskRect
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
@@ -104,51 +87,55 @@ Item {
} }
} }
ScreencopyView { ClippingRectangle {
id: windowPreview
anchors.fill: parent anchors.fill: parent
captureSource: root.overviewOpen ? root.toplevel?.wayland : null radius: Theme.cornerRadius
live: true color: "transparent"
Rectangle { ScreencopyView {
id: windowPreview
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius captureSource: root.overviewOpen ? root.toplevel?.wayland : null
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : live: true
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
ColumnLayout { Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.fill: parent
anchors.left: parent.left radius: Theme.cornerRadius
anchors.right: parent.right color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
spacing: Theme.fontSizeSmall * 0.5 border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
Image { ColumnLayout {
id: windowIcon anchors.verticalCenter: parent.verticalCenter
property var iconSize: { anchors.left: parent.left
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1) anchors.right: parent.right
} spacing: Theme.fontSizeSmall * 0.5
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Behavior on width { Image {
NumberAnimation { id: windowIcon
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) property var iconSize: {
easing.type: Easing.BezierSpline return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
easing.bezierCurve: Theme.variantModalEnterCurve
} }
} Layout.alignment: Qt.AlignHCenter
Behavior on height { source: root.iconPath
NumberAnimation { width: iconSize
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) height: iconSize
easing.type: Easing.BezierSpline sourceSize: Qt.size(iconSize, iconSize)
easing.bezierCurve: Theme.variantModalEnterCurve
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
}
} }
} }
} }
@@ -0,0 +1,73 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Common
import qs.Services
// Accent color extracted from the current track's album art via ColorQuantizer,
// falling back to Theme.primary when no usable accent is available.
Singleton {
id: root
readonly property bool hasAccent: _accent !== null
readonly property color accent: _accent !== null ? _accent : Theme.primary
readonly property color onAccent: {
const c = accent;
const lum = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
return lum > 0.6 ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1);
}
readonly property color accentHover: Theme.withAlpha(accent, 0.12)
readonly property color accentPressed: Theme.withAlpha(accent, Theme.transparentBlurLayers ? 0.24 : 0.16)
// Plain-named alias: underscore-prefixed props with onChanged handlers crash config load.
readonly property string artUrl: TrackArtService.resolvedArtUrl
onArtUrlChanged: {
if (artUrl === "")
_accent = null;
}
property var _accent: null
ColorQuantizer {
id: quantizer
source: root.artUrl
depth: 4
rescaleSize: 64
onColorsChanged: root._accent = root._pickAccent(colors)
}
function _pickAccent(colors) {
if (!colors || colors.length === 0)
return null;
let best = null;
let bestScore = -1;
for (let i = 0; i < colors.length; i++) {
const c = colors[i];
const s = c.hsvSaturation;
const v = c.hsvValue;
if (v < 0.22 || v > 0.96 || s < 0.22)
continue;
const score = s * (1 - Math.abs(v - 0.68));
if (score > bestScore) {
bestScore = score;
best = c;
}
}
if (!best)
return null;
return _normalize(best);
}
function _normalize(c) {
const hue = c.hsvHue < 0 ? 0 : c.hsvHue;
const s = Math.min(1, c.hsvSaturation * 1.05);
const v = Math.max(c.hsvValue, 0.62);
return Qt.hsva(hue, s, v, 1);
}
}
+115
View File
@@ -0,0 +1,115 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float widthPx;
float heightPx;
float value;
float actualValue;
float phase;
float ampPx;
float wavelengthPx;
float lineWidthPx;
float showActual;
vec4 fillColor;
vec4 trackColor;
vec4 playheadColor;
vec4 actualColor;
} ubuf;
const float TAU = 6.28318530718;
const float AA = 0.75; // pixel-space antialias band
// Signed distance to a rounded box centered at the origin.
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + vec2(r);
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
// Composite a straight-alpha color over a premultiplied accumulator.
vec4 blendOver(vec4 dst, vec3 rgb, float a) {
return vec4(rgb * a + dst.rgb * (1.0 - a), a + dst.a * (1.0 - a));
}
void main() {
float w = ubuf.widthPx;
float h = ubuf.heightPx;
vec2 px = vec2(qt_TexCoord0.x * w, qt_TexCoord0.y * h);
float mid = h * 0.5;
float halfW = ubuf.lineWidthPx * 0.5;
float k = TAU / max(ubuf.wavelengthPx, 1e-3);
float playX = clamp(ubuf.value, 0.0, 1.0) * w;
float actualX = clamp(ubuf.actualValue, 0.0, 1.0) * w;
bool seeking = ubuf.showActual > 0.5;
float loX = min(playX, actualX);
float hiX = max(playX, actualX);
float fillEnd = seeking ? loX : playX; // filled progress ends here
float actStart = seeking ? loX : playX; // seek-preview segment
float actEnd = seeking ? hiX : playX;
float trackStart = seeking ? hiX : playX; // unplayed remainder
// Perpendicular distance to the animated sine stroke.
float ang = k * px.x + ubuf.phase;
float wy = mid + ubuf.ampPx * sin(ang);
float slope = ubuf.ampPx * k * cos(ang);
float dWave = abs(px.y - wy) / sqrt(1.0 + slope * slope);
float aaW = AA;
float waveStroke = 1.0 - smoothstep(halfW - aaW, halfW + aaW, dWave);
// Straight remainder line.
float dLine = abs(px.y - mid);
float aaL = AA;
float lineStroke = 1.0 - smoothstep(halfW - aaL, halfW + aaL, dLine);
vec4 col = vec4(0.0);
// 1. Track (unplayed remainder), to the right of the progress head.
{
float m = lineStroke * step(trackStart, px.x);
col = blendOver(col, ubuf.trackColor.rgb, ubuf.trackColor.a * m);
}
// 2. Seek-preview segment (only while seeking).
if (seeking) {
float m = waveStroke * step(actStart, px.x) * step(px.x, actEnd);
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
}
// 3. Filled progress wave.
{
float m = waveStroke * step(halfW, px.x) * step(px.x, fillEnd);
// Rounded start cap.
float capS = length(px - vec2(halfW, mid + ubuf.ampPx * sin(k * halfW + ubuf.phase))) - halfW;
float capM = 1.0 - smoothstep(-aaW, aaW, capS);
m = max(m, capM * step(halfW - 1.0, px.x));
col = blendOver(col, ubuf.fillColor.rgb, ubuf.fillColor.a * m);
}
// 4. Actual-position marker (only while seeking).
if (seeking) {
float amH = max(ubuf.lineWidthPx + 4.0, 10.0);
float d = sdRoundBar(px - vec2(actualX, mid), vec2(1.0, amH * 0.5), 1.0);
float aa = AA;
float m = 1.0 - smoothstep(-aa, aa, d);
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
}
// 5. Playhead pill (on top).
{
float phW = 3.5;
float phH = max(ubuf.lineWidthPx + 12.0, 16.0);
float d = sdRoundBar(px - vec2(playX, mid), vec2(phW * 0.5, phH * 0.5), phW * 0.5);
float aa = AA;
float m = 1.0 - smoothstep(-aa, aa, d);
col = blendOver(col, ubuf.playheadColor.rgb, ubuf.playheadColor.a * m);
}
fragColor = col * ubuf.qt_Opacity;
}
Binary file not shown.
+3 -2
View File
@@ -152,7 +152,8 @@ Item {
property real energy: 0 property real energy: 0
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0) property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
property vector2d bandsB: Qt.vector2d(0, 0) property vector2d bandsB: Qt.vector2d(0, 0)
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a) readonly property color accentColor: MediaAccentService.accent
property vector4d fillColor: Qt.vector4d(accentColor.r, accentColor.g, accentColor.b, accentColor.a)
Behavior on activation { Behavior on activation {
NumberAnimation { NumberAnimation {
@@ -173,7 +174,7 @@ Item {
imageSource: artUrl || lastValidArtUrl || "" imageSource: artUrl || lastValidArtUrl || ""
fallbackIcon: "album" fallbackIcon: "album"
border.color: Theme.primary border.color: MediaAccentService.accent
border.width: 2 border.width: 2
onImageSourceChanged: { onImageSourceChanged: {
+35 -58
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import QtQuick.Effects import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -65,67 +65,44 @@ Rectangle {
border.color: "transparent" border.color: "transparent"
border.width: 0 border.width: 0
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready. ClippingRectangle {
AnimatedImage {
id: probe
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 anchors.margins: 2
asynchronous: true radius: Math.min(width, height) / 2
fillMode: Image.PreserveAspectCrop color: "transparent"
smooth: true
mipmap: true
cache: root.cacheImages
visible: false
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
}
// Takes over once the probe settles on a non-animated image, then latches. // Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
Image { AnimatedImage {
id: staticImage id: probe
anchors.fill: parent
anchors.margins: 2
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: false
sourceSize.width: Math.max(width * 2, 128)
sourceSize.height: Math.max(height * 2, 128)
source: {
if (!root.shouldProbe)
return root.imageSource;
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
return root.imageSource;
return "";
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: root.activeImage
maskEnabled: true
maskSource: circularMask
visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent anchors.fill: parent
radius: width / 2 asynchronous: true
color: "black" fillMode: Image.PreserveAspectCrop
antialiasing: true smooth: true
mipmap: true
cache: root.cacheImages
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
}
// Takes over once the probe settles on a non-animated image, then latches.
Image {
id: staticImage
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
sourceSize.width: Math.max(width * 2, 128)
sourceSize.height: Math.max(height * 2, 128)
source: {
if (!root.shouldProbe)
return root.imageSource;
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
return root.imageSource;
return "";
}
} }
} }
+4 -2
View File
@@ -142,6 +142,8 @@ Item {
value: root.value value: root.value
actualValue: root.playerValue actualValue: root.playerValue
showActualPlaybackState: root.isSeeking showActualPlaybackState: root.isSeeking
fillColor: MediaAccentService.accent
playheadColor: MediaAccentService.accent
actualProgressColor: Theme.onSurface_38 actualProgressColor: Theme.onSurface_38
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
@@ -179,8 +181,8 @@ Item {
Item { Item {
property real lineWidth: 3 property real lineWidth: 3
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40) property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
property color fillColor: Theme.primary property color fillColor: MediaAccentService.accent
property color playheadColor: Theme.primary property color playheadColor: MediaAccentService.accent
property color actualProgressColor: Theme.onSurface_38 property color actualProgressColor: Theme.onSurface_38
readonly property real midY: height / 2 readonly property real midY: height / 2
+5
View File
@@ -256,6 +256,11 @@ FocusScope {
} }
} }
function snapIndicator() {
indicator.initialSetupComplete = false;
updateIndicator();
}
onCurrentIndexChanged: { onCurrentIndexChanged: {
Qt.callLater(updateIndicator); Qt.callLater(updateIndicator);
} }
+20 -234
View File
@@ -1,7 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Shapes
import qs.Common import qs.Common
// Wave progress indicator: track, animated fill, seek preview and playhead are
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
Item { Item {
id: root id: root
@@ -19,19 +20,6 @@ Item {
property color playheadColor: Theme.primary property color playheadColor: Theme.primary
property color actualProgressColor: Theme.onSurface_38 property color actualProgressColor: Theme.onSurface_38
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
function snap(v) {
return Math.round(v * dpr) / dpr;
}
readonly property real playX: snap(root.width * root.value)
readonly property real actualX: snap(root.width * root.actualValue)
readonly property real midY: snap(height / 2)
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
Behavior on currentAmp { Behavior on currentAmp {
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
@@ -39,236 +27,34 @@ Item {
} }
} }
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0 onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
Component.onCompleted: currentAmp = isPlaying ? amp : 0
Shape { ShaderEffect {
id: flatTrack
anchors.fill: parent anchors.fill: parent
antialiasing: true blending: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
ShapePath { readonly property real widthPx: width
strokeColor: root.trackColor readonly property real heightPx: height
strokeWidth: snap(root.lineWidth) readonly property real value: root.value
capStyle: ShapePath.RoundCap readonly property real actualValue: root.actualValue
joinStyle: ShapePath.RoundJoin readonly property real phase: root.phase
fillColor: "transparent" readonly property real ampPx: root.currentAmp
PathMove { readonly property real wavelengthPx: root.wavelength
id: flatStart readonly property real lineWidthPx: root.lineWidth
x: Math.min(root.width, snap(root.playX + playhead.width / 2)) readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
y: root.midY readonly property color fillColor: root.fillColor
} readonly property color trackColor: root.trackColor
PathLine { readonly property color playheadColor: root.playheadColor
id: flatEnd readonly property color actualColor: root.actualProgressColor
x: root.width
y: root.midY fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
}
}
} }
Item {
id: waveClip
anchors.fill: parent
clip: true
readonly property real startX: snap(root.lineWidth / 2)
readonly property real aaBias: (0.25 / root.dpr)
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
Rectangle {
id: mask
anchors.top: parent.top
anchors.bottom: parent.bottom
x: 0
width: waveClip.endX
color: "transparent"
clip: true
Shape {
id: waveShape
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width + 4 * root.wavelength
antialiasing: true
preferredRendererType: Shape.CurveRenderer
x: waveOffsetX
ShapePath {
id: wavePath
strokeColor: root.fillColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
id: waveSvg
path: ""
}
}
}
}
Rectangle {
id: actualMask
anchors.top: parent.top
anchors.bottom: parent.bottom
x: waveClip.gapStartX
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
color: "transparent"
clip: true
visible: (root.previewBehind || root.previewAhead) && width > 0
Shape {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.width + 4 * root.wavelength
antialiasing: true
preferredRendererType: Shape.CurveRenderer
x: waveOffsetX
ShapePath {
strokeColor: root.actualProgressColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: waveSvg.path
}
}
}
}
Rectangle {
id: startCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.fillColor
x: waveClip.startX - width / 2
y: waveY(waveClip.startX) - height / 2
visible: waveClip.endX > waveClip.startX
z: 2
}
Rectangle {
id: endCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.fillColor
x: waveClip.endX - width / 2
y: waveY(waveClip.endX) - height / 2
visible: waveClip.endX > waveClip.startX
z: 2
}
Rectangle {
id: actualEndCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.actualProgressColor
x: waveClip.gapEndX - width / 2
y: waveY(waveClip.gapEndX) - height / 2
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
z: 2
}
Rectangle {
id: actualMarker
width: 2
height: Math.max(root.lineWidth + 4, 10)
radius: width / 2
color: root.actualProgressColor
x: root.actualX - width / 2
y: root.midY - height / 2
visible: root.showActualPlaybackState
z: 2
}
}
Rectangle {
id: playhead
width: 3.5
height: Math.max(root.lineWidth + 12, 16)
radius: width / 2
color: root.playheadColor
x: root.playX - width / 2
y: root.midY - height / 2
z: 3
}
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
function wrapMod(a, m) {
let r = a % m;
return r < 0 ? r + m : r;
}
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
}
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
FrameAnimation { FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0) running: root.visible && (root.isPlaying || root.currentAmp > 0)
onTriggered: { onTriggered: {
if (root.isPlaying) if (root.isPlaying)
root.phase += 0.03 * frameTime * 60; root.phase += 0.03 * frameTime * 60;
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
} }
} }
function buildStaticWave() {
const start = waveClip.startX - 2 * root.wavelength;
const end = width + 2 * root.wavelength;
if (end <= start) {
waveSvg.path = "";
return;
}
const kLocal = k;
const halfPeriod = root.wavelength / 2;
function y0(x) {
return root.midY + root.currentAmp * Math.sin(kLocal * x);
}
function dy0(x) {
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
}
let x0 = start;
let d = `M ${x0} ${y0(x0)}`;
while (x0 < end) {
const x1 = Math.min(x0 + halfPeriod, end);
const dx = x1 - x0;
const yA = y0(x0), yB = y0(x1);
const dyA = dy0(x0), dyB = dy0(x1);
const c1x = x0 + dx / 3;
const c1y = yA + (dyA * dx) / 3;
const c2x = x1 - dx / 3;
const c2y = yB - (dyB * dx) / 3;
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
x0 = x1;
}
waveSvg.path = d;
}
Component.onCompleted: {
currentAmp = isPlaying ? amp : 0;
buildStaticWave();
}
onWidthChanged: {
flatEnd.x = width;
buildStaticWave();
}
onHeightChanged: buildStaticWave()
onCurrentAmpChanged: buildStaticWave()
onWavelengthChanged: {
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
buildStaticWave();
}
} }