1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18: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.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -591,24 +591,11 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: gridProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -729,24 +716,11 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: listProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -240,8 +240,10 @@ DankPopout {
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
mainContainer.forceActiveFocus();
if (!root.shouldBeVisible)
return;
mainContainer.forceActiveFocus();
tabBar.snapIndicator();
}
}
+24 -11
View File
@@ -25,6 +25,11 @@ Item {
property string section: ""
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 showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
@@ -298,8 +303,16 @@ Item {
Component.onCompleted: syncArt()
function syncArt() {
if (curArt === "" || layerA.art == curArt || layerB.art == curArt)
if (curArt === "")
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)
layerB.art = curArt;
else
@@ -564,13 +577,13 @@ Item {
height: 40
radius: 20
anchors.centerIn: parent
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
DankIcon {
anchors.centerIn: parent
name: "shuffle"
size: 20
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
}
MouseArea {
@@ -626,13 +639,13 @@ Item {
height: 50
radius: 25
anchors.centerIn: parent
color: Theme.primary
color: root.accent
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28
color: Theme.background
color: root.onAccent
weight: 500
}
@@ -696,7 +709,7 @@ Item {
height: 40
radius: 20
anchors.centerIn: parent
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
DankIcon {
anchors.centerIn: parent
@@ -713,7 +726,7 @@ Item {
}
}
size: 20
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
}
MouseArea {
@@ -752,7 +765,7 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
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.width: 1
z: 100
@@ -819,7 +832,7 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
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.width: 1
z: 101
@@ -831,7 +844,7 @@ Item {
anchors.centerIn: parent
name: getVolumeIcon()
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 {
@@ -882,7 +895,7 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
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.width: 1
z: 100
@@ -154,13 +154,13 @@ Card {
width: 32
height: 32
radius: 16
color: Theme.primary
color: MediaAccentService.accent
DankIcon {
anchors.centerIn: parent
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 16
color: Theme.background
color: MediaAccentService.onAccent
}
MouseArea {
+5 -31
View File
@@ -1,7 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
@@ -567,24 +567,11 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: gridProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
@@ -700,24 +687,11 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0
Rectangle {
id: listProgressMask
ClippingRectangle {
anchors.fill: parent
radius: parent.radius
visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
color: "transparent"
visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle {
anchors.left: parent.left
+5 -21
View File
@@ -4,6 +4,7 @@ import qs.Common
import qs.Services
import qs.Widgets
import Quickshell.Services.Mpris
import Quickshell.Widgets
DankOSD {
id: root
@@ -185,10 +186,11 @@ DankOSD {
visible: false
}
Item {
id: blurredBg
ClippingRectangle {
anchors.fill: parent
visible: false
radius: Theme.cornerRadius
color: "transparent"
opacity: 0.7
MultiEffect {
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 {
anchors.fill: parent
radius: Theme.cornerRadius
+20 -31
View File
@@ -1,7 +1,7 @@
import QtCore
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
@@ -520,28 +520,27 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = Theme.wallpaperPath;
if (!wp || wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
sourceSize.width: 120
sourceSize.height: 120
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: autoWallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = Theme.wallpaperPath;
if (!wp || wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
sourceSize.width: 120
sourceSize.height: 120
asynchronous: true
}
}
@@ -553,16 +552,6 @@ Item {
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 {
anchors.centerIn: parent
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
+64 -97
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
@@ -75,28 +75,27 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = root.currentWallpaper;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: wallpaperMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = root.currentWallpaper;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
}
}
@@ -108,16 +107,6 @@ Item {
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 {
anchors.centerIn: parent
name: "image"
@@ -421,31 +410,30 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = SessionData.wallpaperPathLight;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var lightWallpaper = SessionData.wallpaperPathLight;
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: lightMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = SessionData.wallpaperPathLight;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var lightWallpaper = SessionData.wallpaperPathLight;
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
}
}
@@ -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 {
anchors.centerIn: parent
name: "light_mode"
@@ -611,31 +589,30 @@ Item {
radius: Theme.cornerRadius
color: Theme.surfaceVariant
Image {
ClippingRectangle {
anchors.fill: parent
anchors.margins: 1
source: {
var wp = SessionData.wallpaperPathDark;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var darkWallpaper = SessionData.wallpaperPathDark;
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: darkMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
radius: Theme.cornerRadius - 1
color: "transparent"
Image {
anchors.fill: parent
source: {
var wp = SessionData.wallpaperPathDark;
if (wp === "" || wp.startsWith("#"))
return "";
if (wp.startsWith("file://"))
wp = wp.substring(7);
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
}
fillMode: Image.PreserveAspectCrop
visible: {
var darkWallpaper = SessionData.wallpaperPathDark;
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
}
sourceSize.width: 160
sourceSize.height: 160
asynchronous: true
}
}
@@ -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 {
anchors.centerIn: parent
name: "dark_mode"
@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
Item {
@@ -58,23 +58,6 @@ Item {
visible: intersectsViewport
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 {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
@@ -104,51 +87,55 @@ Item {
}
}
ScreencopyView {
id: windowPreview
ClippingRectangle {
anchors.fill: parent
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
live: true
radius: Theme.cornerRadius
color: "transparent"
Rectangle {
ScreencopyView {
id: windowPreview
anchors.fill: parent
radius: Theme.cornerRadius
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
live: true
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.fontSizeSmall * 0.5
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
Image {
id: windowIcon
property var iconSize: {
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
}
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.fontSizeSmall * 0.5
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
Image {
id: windowIcon
property var iconSize: {
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
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 vector4d bandsA: Qt.vector4d(0, 0, 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 {
NumberAnimation {
@@ -173,7 +174,7 @@ Item {
imageSource: artUrl || lastValidArtUrl || ""
fallbackIcon: "album"
border.color: Theme.primary
border.color: MediaAccentService.accent
border.width: 2
onImageSourceChanged: {
+35 -58
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Window
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.Widgets
@@ -65,67 +65,44 @@ Rectangle {
border.color: "transparent"
border.width: 0
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
AnimatedImage {
id: probe
ClippingRectangle {
anchors.fill: parent
anchors.margins: 2
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: false
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
}
radius: Math.min(width, height) / 2
color: "transparent"
// Takes over once the probe settles on a non-animated image, then latches.
Image {
id: staticImage
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 {
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
AnimatedImage {
id: probe
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
asynchronous: true
fillMode: Image.PreserveAspectCrop
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
actualValue: root.playerValue
showActualPlaybackState: root.isSeeking
fillColor: MediaAccentService.accent
playheadColor: MediaAccentService.accent
actualProgressColor: Theme.onSurface_38
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
@@ -179,8 +181,8 @@ Item {
Item {
property real lineWidth: 3
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
property color fillColor: MediaAccentService.accent
property color playheadColor: MediaAccentService.accent
property color actualProgressColor: Theme.onSurface_38
readonly property real midY: height / 2
+5
View File
@@ -256,6 +256,11 @@ FocusScope {
}
}
function snapIndicator() {
indicator.initialSetupComplete = false;
updateIndicator();
}
onCurrentIndexChanged: {
Qt.callLater(updateIndicator);
}
+20 -234
View File
@@ -1,7 +1,8 @@
import QtQuick
import QtQuick.Shapes
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 {
id: root
@@ -19,19 +20,6 @@ Item {
property color playheadColor: Theme.primary
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 {
NumberAnimation {
duration: 300
@@ -39,236 +27,34 @@ Item {
}
}
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
Component.onCompleted: currentAmp = isPlaying ? amp : 0
Shape {
id: flatTrack
ShaderEffect {
anchors.fill: parent
antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
blending: true
ShapePath {
strokeColor: root.trackColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathMove {
id: flatStart
x: Math.min(root.width, snap(root.playX + playhead.width / 2))
y: root.midY
}
PathLine {
id: flatEnd
x: root.width
y: root.midY
}
}
readonly property real widthPx: width
readonly property real heightPx: height
readonly property real value: root.value
readonly property real actualValue: root.actualValue
readonly property real phase: root.phase
readonly property real ampPx: root.currentAmp
readonly property real wavelengthPx: root.wavelength
readonly property real lineWidthPx: root.lineWidth
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
readonly property color fillColor: root.fillColor
readonly property color trackColor: root.trackColor
readonly property color playheadColor: root.playheadColor
readonly property color actualColor: root.actualProgressColor
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 {
running: root.visible && (root.isPlaying || root.currentAmp > 0)
onTriggered: {
if (root.isPlaying)
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();
}
}