diff --git a/quickshell/Common/DankAnim.qml b/quickshell/Common/DankAnim.qml new file mode 100644 index 00000000..63d0c1c3 --- /dev/null +++ b/quickshell/Common/DankAnim.qml @@ -0,0 +1,9 @@ +import QtQuick +import qs.Common + +// Reusable NumberAnimation wrapper +NumberAnimation { + duration: Theme.expressiveDurations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.standard +} diff --git a/quickshell/Common/DankColorAnim.qml b/quickshell/Common/DankColorAnim.qml new file mode 100644 index 00000000..5bd5a9d4 --- /dev/null +++ b/quickshell/Common/DankColorAnim.qml @@ -0,0 +1,9 @@ +import QtQuick +import qs.Common + +// Reusable ColorAnimation wrapper +ColorAnimation { + duration: Theme.expressiveDurations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.standard +} diff --git a/quickshell/Common/ListViewTransitions.qml b/quickshell/Common/ListViewTransitions.qml new file mode 100644 index 00000000..420f5dd9 --- /dev/null +++ b/quickshell/Common/ListViewTransitions.qml @@ -0,0 +1,60 @@ +pragma Singleton +import QtQuick +import qs.Common + +// Reusable ListView/GridView transitions +QtObject { + id: root + + readonly property Transition add: Transition { + ParallelAnimation { + DankAnim { + property: "opacity" + from: 0 + to: 1 + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + } + DankAnim { + property: "scale" + from: 0.92 + to: 1 + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel + } + } + } + + readonly property Transition remove: Transition { + ParallelAnimation { + DankAnim { + property: "opacity" + to: 0 + duration: Theme.expressiveDurations.fast + easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel + } + DankAnim { + property: "scale" + to: 0.92 + duration: Theme.expressiveDurations.fast + easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel + } + } + } + + readonly property Transition displaced: Transition { + DankAnim { + properties: "x,y" + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial + } + } + + readonly property Transition move: Transition { + DankAnim { + properties: "x,y" + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.bezierCurve: Theme.expressiveCurves.standard + } + } +} diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 6779d955..5dbf821e 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -154,10 +154,14 @@ Singleton { property bool nightModeEnabled: false property int animationSpeed: SettingsData.AnimationSpeed.Short property int customAnimationDuration: 500 + property bool syncComponentAnimationSpeeds: true + onSyncComponentAnimationSpeedsChanged: saveSettings() property int popoutAnimationSpeed: SettingsData.AnimationSpeed.Short property int popoutCustomAnimationDuration: 150 property int modalAnimationSpeed: SettingsData.AnimationSpeed.Short property int modalCustomAnimationDuration: 150 + property bool enableRippleEffects: true + onEnableRippleEffectsChanged: saveSettings() property string wallpaperFillMode: "Fill" property bool blurredWallpaperLayer: false property bool blurWallpaperOnOverview: false diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 15a302d7..752554ed 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -769,6 +769,9 @@ Singleton { readonly property int popoutAnimationDuration: { if (typeof SettingsData === "undefined") return 150; + if (SettingsData.syncComponentAnimationSpeeds) { + return Math.min(currentAnimationBaseDuration, 1000); + } const presetMap = [0, 150, 300, 500]; if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom) return SettingsData.popoutCustomAnimationDuration; @@ -778,6 +781,9 @@ Singleton { readonly property int modalAnimationDuration: { if (typeof SettingsData === "undefined") return 150; + if (SettingsData.syncComponentAnimationSpeeds) { + return Math.min(currentAnimationBaseDuration, 1000); + } const presetMap = [0, 150, 300, 500]; if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom) return SettingsData.modalCustomAnimationDuration; diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 1d63053b..6ee7da85 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -40,10 +40,12 @@ var SPEC = { nightModeEnabled: { def: false }, animationSpeed: { def: 1 }, customAnimationDuration: { def: 500 }, + syncComponentAnimationSpeeds: { def: true }, popoutAnimationSpeed: { def: 1 }, popoutCustomAnimationDuration: { def: 150 }, modalAnimationSpeed: { def: 1 }, modalCustomAnimationDuration: { def: 150 }, + enableRippleEffects: { def: true }, wallpaperFillMode: { def: "Fill" }, blurredWallpaperLayer: { def: false }, blurWallpaperOnOverview: { def: false }, diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index 61af958f..cce215b2 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -284,9 +284,8 @@ Item { Behavior on opacity { enabled: root.animationsEnabled - NumberAnimation { + DankAnim { duration: root.animationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } @@ -332,27 +331,24 @@ Item { Behavior on animX { enabled: root.animationsEnabled - NumberAnimation { + DankAnim { duration: root.animationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Behavior on animY { enabled: root.animationsEnabled - NumberAnimation { + DankAnim { duration: root.animationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Behavior on scaleValue { enabled: root.animationsEnabled - NumberAnimation { + DankAnim { duration: root.animationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 71481ffa..5414335a 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -309,9 +309,8 @@ Item { visible: contentVisible || opacity > 0 Behavior on opacity { - NumberAnimation { + DankAnim { duration: Theme.modalAnimationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized } } @@ -345,17 +344,15 @@ Item { transformOrigin: Item.Center Behavior on opacity { - NumberAnimation { + DankAnim { duration: Theme.modalAnimationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized } } Behavior on scale { - NumberAnimation { + DankAnim { duration: Theme.modalAnimationDuration - easing.type: Easing.BezierSpline easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 90b98bcf..92cc9af3 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -718,7 +718,7 @@ PanelWindow { target: content property: "swipeOffset" to: isTopCenter ? -content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width) - duration: Anims.durShort + duration: Theme.shortDuration easing.type: Easing.OutCubic onStopped: { NotificationService.dismissNotification(notificationData); @@ -757,9 +757,9 @@ PanelWindow { return isLeft ? -Anims.slidePx : Anims.slidePx; } to: 0 - duration: Anims.durMed + duration: Theme.mediumDuration easing.type: Easing.BezierSpline - easing.bezierCurve: isTopCenter ? Anims.standardDecel : Anims.emphasizedDecel + easing.bezierCurve: isTopCenter ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel onStopped: { if (!win.exiting && !win._isDestroying) { if (isTopCenter) { @@ -788,9 +788,9 @@ PanelWindow { const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; return isLeft ? -Anims.slidePx : Anims.slidePx; } - duration: Anims.durShort + duration: Theme.shortDuration easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedAccel + easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel } NumberAnimation { @@ -798,9 +798,9 @@ PanelWindow { property: "opacity" from: 1 to: 0 - duration: Anims.durShort + duration: Theme.shortDuration easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standardAccel + easing.bezierCurve: Theme.expressiveCurves.standardAccel } NumberAnimation { @@ -808,9 +808,9 @@ PanelWindow { property: "scale" from: 1 to: 0.98 - duration: Anims.durShort + duration: Theme.shortDuration easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.emphasizedAccel + easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel } } @@ -867,9 +867,9 @@ PanelWindow { enabled: !exiting && !_isDestroying NumberAnimation { - duration: Anims.durShort + duration: Theme.shortDuration easing.type: Easing.BezierSpline - easing.bezierCurve: Anims.standardDecel + easing.bezierCurve: Theme.expressiveCurves.standardDecel } } } diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index 45704dfe..8648acb4 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -239,10 +239,10 @@ Item { tab: "typography" tags: ["animation", "duration", "custom", "speed"] settingKey: "customAnimationDuration" - text: I18n.tr("Custom Duration") - description: I18n.tr("Fine-tune animation timing in milliseconds") + text: I18n.tr("Animation Duration") + description: I18n.tr("Globally scale all animation durations") minimum: 0 - maximum: 750 + maximum: 1000 value: Theme.currentAnimationBaseDuration unit: "ms" defaultValue: 200 @@ -269,11 +269,34 @@ Item { } } } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.15 + } + + SettingsToggleRow { + tab: "typography" + tags: ["animation", "sync", "popout", "modal", "global"] + settingKey: "syncComponentAnimationSpeeds" + text: I18n.tr("Sync Popouts & Modals") + description: I18n.tr("Popouts and Modals follow global Animation Speed (disable to customize independently)") + checked: SettingsData.syncComponentAnimationSpeeds + onToggled: checked => SettingsData.set("syncComponentAnimationSpeeds", checked) + + Connections { + target: SettingsData + function onSyncComponentAnimationSpeedsChanged() { + } + } + } } SettingsCard { tab: "typography" - tags: ["animation", "speed", "motion", "duration", "popout"] + tags: ["animation", "speed", "motion", "duration", "popout", "sync"] title: I18n.tr("%1 Animation Speed").arg(I18n.tr("Popouts")) settingKey: "popoutAnimationSpeed" iconName: "open_in_new" @@ -295,6 +318,8 @@ Item { onSelectionChanged: (index, selected) => { if (!selected) return; + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); SettingsData.set("popoutAnimationSpeed", index); } @@ -322,11 +347,13 @@ Item { text: I18n.tr("Custom Duration") description: I18n.tr("%1 custom animation duration").arg(I18n.tr("Popouts")) minimum: 0 - maximum: 2000 + maximum: 1000 value: Theme.popoutAnimationDuration unit: "ms" defaultValue: 150 onSliderValueChanged: newValue => { + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); SettingsData.set("popoutAnimationSpeed", SettingsData.AnimationSpeed.Custom); SettingsData.set("popoutCustomAnimationDuration", newValue); } @@ -343,7 +370,7 @@ Item { Connections { target: Theme function onPopoutAnimationDurationChanged() { - if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom) + if (!SettingsData.syncComponentAnimationSpeeds && SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom) return; popoutDurationSlider.value = Theme.popoutAnimationDuration; } @@ -353,7 +380,7 @@ Item { SettingsCard { tab: "typography" - tags: ["animation", "speed", "motion", "duration", "modal"] + tags: ["animation", "speed", "motion", "duration", "modal", "sync"] title: I18n.tr("%1 Animation Speed").arg(I18n.tr("Modals")) settingKey: "modalAnimationSpeed" iconName: "web_asset" @@ -375,6 +402,8 @@ Item { onSelectionChanged: (index, selected) => { if (!selected) return; + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); SettingsData.set("modalAnimationSpeed", index); } @@ -402,11 +431,13 @@ Item { text: I18n.tr("Custom Duration") description: I18n.tr("%1 custom animation duration").arg(I18n.tr("Modals")) minimum: 0 - maximum: 2000 + maximum: 1000 value: Theme.modalAnimationDuration unit: "ms" defaultValue: 150 onSliderValueChanged: newValue => { + if (SettingsData.syncComponentAnimationSpeeds) + SettingsData.set("syncComponentAnimationSpeeds", false); SettingsData.set("modalAnimationSpeed", SettingsData.AnimationSpeed.Custom); SettingsData.set("modalCustomAnimationDuration", newValue); } @@ -423,13 +454,37 @@ Item { Connections { target: Theme function onModalAnimationDurationChanged() { - if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom) + if (!SettingsData.syncComponentAnimationSpeeds && SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom) return; modalDurationSlider.value = Theme.modalAnimationDuration; } } } } + + SettingsCard { + tab: "typography" + tags: ["animation", "ripple", "effect", "material", "feedback"] + title: I18n.tr("Ripple Effects") + settingKey: "enableRippleEffects" + iconName: "radio_button_unchecked" + + SettingsToggleRow { + tab: "typography" + tags: ["animation", "ripple", "effect", "material", "click"] + settingKey: "enableRippleEffects" + text: I18n.tr("Enable Ripple Effects") + description: I18n.tr("Show Material Design ripple animations on interactive elements") + checked: SettingsData.enableRippleEffects ?? true + onToggled: newValue => SettingsData.set("enableRippleEffects", newValue) + + Connections { + target: SettingsData + function onEnableRippleEffectsChanged() { + } + } + } + } } } } diff --git a/quickshell/Widgets/DankButton.qml b/quickshell/Widgets/DankButton.qml index d3ac137c..ecedf83a 100644 --- a/quickshell/Widgets/DankButton.qml +++ b/quickshell/Widgets/DankButton.qml @@ -15,6 +15,8 @@ Rectangle { property color textColor: Theme.buttonText property int buttonHeight: 40 property int horizontalPadding: Theme.spacingL + property bool enableScaleAnimation: false + property bool enableRipple: false signal clicked @@ -23,6 +25,15 @@ Rectangle { radius: Theme.cornerRadius color: backgroundColor opacity: enabled ? 1 : 0.4 + scale: (enableScaleAnimation && pressed) ? 0.98 : 1.0 + + Behavior on scale { + enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankAnim { + duration: 100 + easing.bezierCurve: Theme.expressiveCurves.standard + } + } Rectangle { id: stateLayer diff --git a/quickshell/Widgets/DankCollapsibleSection.qml b/quickshell/Widgets/DankCollapsibleSection.qml new file mode 100644 index 00000000..0ed0e59c --- /dev/null +++ b/quickshell/Widgets/DankCollapsibleSection.qml @@ -0,0 +1,130 @@ +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Widgets + +ColumnLayout { + id: root + + required property string title + property string description: "" + property bool expanded: false + property bool showBackground: false + property alias headerColor: headerRect.color + + signal toggleRequested + + spacing: Theme.spacingS + Layout.fillWidth: true + + Rectangle { + id: headerRect + Layout.fillWidth: true + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Theme.paddingM * 2, 48) + radius: Theme.cornerRadius + color: "transparent" + + RowLayout { + id: titleRow + anchors.fill: parent + anchors.leftMargin: Theme.paddingM + anchors.rightMargin: Theme.paddingM + spacing: Theme.spacingM + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + Layout.fillWidth: true + } + + DankIcon { + name: "expand_more" + size: Theme.iconSizeSmall + rotation: root.expanded ? 180 : 0 + + Behavior on rotation { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankAnim { + duration: Theme.shortDuration + easing.bezierCurve: Theme.expressiveCurves.standard + } + } + } + } + + StateLayer { + anchors.fill: parent + onClicked: { + root.toggleRequested(); + root.expanded = !root.expanded; + } + } + } + + default property alias content: contentColumn.data + + Item { + id: contentWrapper + Layout.fillWidth: true + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Theme.spacingS * 2) : 0 + clip: true + + Behavior on Layout.preferredHeight { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankAnim { + duration: Theme.expressiveDurations.expressiveDefaultSpatial + easing.bezierCurve: Theme.expressiveCurves.standard + } + } + + Rectangle { + id: backgroundRect + anchors.fill: parent + radius: Theme.cornerRadius + color: Theme.surfaceContainer + opacity: root.showBackground && root.expanded ? 1.0 : 0.0 + visible: root.showBackground + + Behavior on opacity { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankAnim { + duration: Theme.shortDuration + easing.bezierCurve: Theme.expressiveCurves.standard + } + } + } + + ColumnLayout { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + y: Theme.spacingS + anchors.leftMargin: Theme.paddingM + anchors.rightMargin: Theme.paddingM + anchors.bottomMargin: Theme.spacingS + spacing: Theme.spacingS + opacity: root.expanded ? 1.0 : 0.0 + + Behavior on opacity { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankAnim { + duration: Theme.shortDuration + easing.bezierCurve: Theme.expressiveCurves.standard + } + } + + StyledText { + id: descriptionText + Layout.fillWidth: true + Layout.topMargin: root.description !== "" ? Theme.spacingXS : 0 + Layout.bottomMargin: root.description !== "" ? Theme.spacingS : 0 + visible: root.description !== "" + text: root.description + color: Theme.surfaceTextSecondary + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + } + } + } +} diff --git a/quickshell/Widgets/DankIcon.qml b/quickshell/Widgets/DankIcon.qml index 0c53cb70..e80b93a5 100644 --- a/quickshell/Widgets/DankIcon.qml +++ b/quickshell/Widgets/DankIcon.qml @@ -34,6 +34,14 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter antialiasing: true + + Behavior on color { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankColorAnim { + duration: Theme.shorterDuration + easing.bezierCurve: Theme.expressiveCurves.standard + } + } font.variableAxes: { "FILL": root.fill.toFixed(1), "GRAD": root.grade, diff --git a/quickshell/Widgets/DankListView.qml b/quickshell/Widgets/DankListView.qml index d22ba4a0..7b490717 100644 --- a/quickshell/Widgets/DankListView.qml +++ b/quickshell/Widgets/DankListView.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import qs.Common import qs.Widgets ListView { @@ -22,6 +23,11 @@ ListView { pressDelay: 0 flickableDirection: Flickable.VerticalFlick + add: ListViewTransitions.add + remove: ListViewTransitions.remove + displaced: ListViewTransitions.displaced + move: ListViewTransitions.move + onMovementStarted: { isUserScrolling = true; vbar._scrollBarActive = true; diff --git a/quickshell/Widgets/DankRipple.qml b/quickshell/Widgets/DankRipple.qml new file mode 100644 index 00000000..1cacbeaa --- /dev/null +++ b/quickshell/Widgets/DankRipple.qml @@ -0,0 +1,91 @@ +import QtQuick +import qs.Common + +// Material Design 3 ripple effect component +MouseArea { + id: root + + property color rippleColor: Theme.primary + property real cornerRadius: 0 + property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true + + property real _rippleX: 0 + property real _rippleY: 0 + property real _rippleRadius: 0 + + enabled: false + hoverEnabled: false + + function trigger(x, y) { + if (!enableRipple || Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None) + return; + + _rippleX = x; + _rippleY = y; + + const dist = (ox, oy) => ox * ox + oy * oy; + _rippleRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y))); + + rippleAnim.restart(); + } + + SequentialAnimation { + id: rippleAnim + + PropertyAction { + target: ripple + property: "x" + value: root._rippleX + } + PropertyAction { + target: ripple + property: "y" + value: root._rippleY + } + PropertyAction { + target: ripple + property: "opacity" + value: 0.08 + } + + ParallelAnimation { + DankAnim { + target: ripple + property: "implicitWidth" + from: 0 + to: root._rippleRadius * 2 + duration: Theme.expressiveDurations.expressiveEffects + easing.bezierCurve: Theme.expressiveCurves.standardDecel + } + DankAnim { + target: ripple + property: "implicitHeight" + from: 0 + to: root._rippleRadius * 2 + duration: Theme.expressiveDurations.expressiveEffects + easing.bezierCurve: Theme.expressiveCurves.standardDecel + } + } + + DankAnim { + target: ripple + property: "opacity" + to: 0 + duration: Theme.expressiveDurations.expressiveEffects + easing.bezierCurve: Theme.expressiveCurves.standard + } + } + + Rectangle { + id: ripple + + radius: Math.min(width, height) / 2 + color: root.rippleColor + opacity: 0 + + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } +} diff --git a/quickshell/Widgets/StateLayer.qml b/quickshell/Widgets/StateLayer.qml index 7bbd7cd2..8969d930 100644 --- a/quickshell/Widgets/StateLayer.qml +++ b/quickshell/Widgets/StateLayer.qml @@ -9,6 +9,7 @@ MouseArea { property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius property var tooltipText: null property string tooltipSide: "bottom" + property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0 @@ -16,10 +17,33 @@ MouseArea { cursorShape: disabled ? undefined : Qt.PointingHandCursor hoverEnabled: true + onPressed: mouse => { + if (!disabled && enableRipple) { + rippleLayer.trigger(mouse.x, mouse.y); + } + } + Rectangle { + id: stateRect anchors.fill: parent radius: root.cornerRadius color: Qt.rgba(stateColor.r, stateColor.g, stateColor.b, stateOpacity) + + Behavior on color { + enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + DankColorAnim { + duration: Theme.shorterDuration + easing.bezierCurve: Theme.expressiveCurves.standardDecel + } + } + } + + DankRipple { + id: rippleLayer + anchors.fill: parent + rippleColor: root.stateColor + cornerRadius: root.cornerRadius + enableRipple: root.enableRipple } Timer {