diff --git a/Components/Anim.qml b/Components/Anim.qml index e5743e5..0b02612 100644 --- a/Components/Anim.qml +++ b/Components/Anim.qml @@ -2,7 +2,62 @@ import QtQuick import qs.Config NumberAnimation { - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.standard - easing.type: Easing.BezierSpline + enum Type { + StandardSmall = 0, + Standard, + StandardLarge, + StandardExtraLarge, + EmphasizedSmall, + Emphasized, + EmphasizedLarge, + EmphasizedExtraLarge, + FastSpatial, + DefaultSpatial, + SlowSpatial, + FastEffects, + DefaultEffects, + SlowEffects + } + + property int type: Anim.DefaultSpatial + + duration: { + if (type < Anim.StandardSmall || type > Anim.SlowEffects) + return Appearance.anim.durations.normal; + + if (type === Anim.FastSpatial) + return Appearance.anim.durations.expressiveFastSpatial; + if (type === Anim.DefaultSpatial) + return Appearance.anim.durations.expressiveDefaultSpatial; + if (type === Anim.SlowSpatial) + return Appearance.anim.durations.large; + if (type === Anim.FastEffects) + return Appearance.anim.durations.expressiveFastEffects; + if (type === Anim.DefaultEffects) + return Appearance.anim.durations.expressiveEffects; + if (type === Anim.SlowEffects) + return Appearance.anim.durations.expressiveSlowEffects; + + const types = ["small", "normal", "large", "extraLarge"]; + const idx = type % 4; // 0-7 are the 4 standard types + return Appearance.anim.durations[types[idx]]; + } + easing.bezierCurve: { + if (type === Anim.FastSpatial) + return Appearance.anim.curves.expressiveFastSpatial; + if (type === Anim.DefaultSpatial) + return Appearance.anim.curves.expressiveDefaultSpatial; + if (type === Anim.SlowSpatial) + return Appearance.anim.curves.expressiveSlowSpatial; + if (type === Anim.FastEffects) + return Appearance.anim.curves.expressiveFastEffects; + if (type === Anim.DefaultEffects) + return Appearance.anim.curves.expressiveDefaultEffects; + if (type === Anim.SlowEffects) + return Appearance.anim.curves.expressiveSlowEffects; + + if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) + return Appearance.anim.curves.emphasized; + return Appearance.anim.curves.standard; + } } diff --git a/Components/CustomSlider.qml b/Components/CustomSlider.qml index 311f861..3041698 100644 --- a/Components/CustomSlider.qml +++ b/Components/CustomSlider.qml @@ -1,51 +1,174 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Templates +import ZShell.Components +import ZShell +import qs.Components import qs.Config Slider { id: root - property color color: DynamicColors.palette.m3primary + property bool animateWave + property color bgColor: enabled ? DynamicColors.palette.m3secondaryContainer : Qt.alpha(DynamicColors.palette.m3onSurface, 0.1) + property color fgColor: enabled ? DynamicColors.palette.m3primary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.38) + property real filledWidth + property real pos: visualPosition + property int waveDuration: 1000 + property real waveFrequency: 6 + property bool wavy + + signal interaction(v: real) + + implicitHeight: 12 + implicitWidth: 200 + + contentItem: Item { + anchors.fill: parent - background: Item { CustomRect { - anchors.bottom: parent.bottom - anchors.bottomMargin: root.implicitHeight / 6 - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: root.implicitHeight / 6 - bottomRightRadius: root.implicitHeight / 6 - color: root.color - implicitWidth: root.handle.x - root.implicitHeight / 6 - radius: root.implicitHeight / 6 - topRightRadius: root.implicitHeight / 6 + id: remaining + + anchors.left: handle.right + anchors.leftMargin: Appearance.spacing.extraSmall + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + bottomLeftRadius: Appearance.rounding.extraSmall / 2 + color: root.bgColor + implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1)) + opacity: Math.min(width, 12) / 12 + radius: Appearance.rounding.small + topLeftRadius: Appearance.rounding.extraSmall / 2 } CustomRect { - anchors.bottom: parent.bottom - anchors.bottomMargin: root.implicitHeight / 6 anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: root.implicitHeight / 6 - bottomLeftRadius: root.implicitHeight / 6 - color: DynamicColors.tPalette.m3surfaceContainerHighest - implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 - radius: root.implicitHeight / 6 - topLeftRadius: root.implicitHeight / 6 + anchors.rightMargin: 4 * remaining.opacity + anchors.verticalCenter: parent.verticalCenter + color: root.fgColor + implicitHeight: 4 * remaining.opacity + implicitWidth: implicitHeight + opacity: remaining.opacity + radius: Appearance.rounding.full + } + + CustomRect { + id: handle + + anchors.left: filled.right + anchors.leftMargin: Appearance.spacing.extraSmall + anchors.verticalCenter: parent.verticalCenter + color: root.fgColor + implicitHeight: { + const mult = parent.height <= 12 ? 3 : 1.2; + const pressMult = parent.height <= 12 ? 4 : 1.5; + return parent.height * (mouse.pressed ? pressMult : mult); + } + implicitWidth: 4 + radius: Appearance.rounding.full + + Behavior on implicitHeight { + Anim { + type: Anim.FastSpatial + } + } + } + + Loader { + id: filled + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + asynchronous: true + sourceComponent: root.wavy ? waveComp : lineComp + } + + Component { + id: lineComp + + CustomRect { + bottomRightRadius: Appearance.rounding.extraSmall / 2 + color: root.fgColor + implicitHeight: root.height + implicitWidth: root.filledWidth + radius: Appearance.rounding.small + topRightRadius: Appearance.rounding.extraSmall / 2 + } + } + + Component { + id: waveComp + + WavyLine { + color: root.fgColor + frequency: root.waveFrequency + fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin + implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth + implicitWidth: root.filledWidth + lineWidth: root.height * 0.7 + startX: x + + Behavior on color { + CAnim { + } + } + Anim on waveProgress { + duration: root.waveDuration + easing.type: Easing.Linear + from: 0 + loops: Animation.Infinite + paused: !root.animateWave + running: true + to: 1 + } + } } } - handle: CustomRect { - anchors.verticalCenter: parent.verticalCenter - color: root.color - implicitHeight: root.implicitHeight - implicitWidth: root.implicitHeight / 4.5 - radius: Appearance.rounding.full - x: root.visualPosition * root.availableWidth - implicitWidth / 2 + Behavior on filledWidth { + id: widthBehavior - MouseArea { - acceptedButtons: Qt.NoButton - anchors.fill: parent - cursorShape: Qt.PointingHandCursor + Anim { + } + } + + Component.onCompleted: filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos) + + Binding { + id: posBinding + + property: "pos" + target: root + value: ZUtils.clamp(mouse.pressStartPos + mouse.dragMovement, 0, 1) + when: mouse.pressed + } + + MouseArea { + id: mouse + + property real dragMovement + property real pressStartPos + property real pressStartX + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + implicitHeight: handle.implicitHeight + preventStealing: true + + onPositionChanged: e => { + dragMovement = (e.x - pressStartX) / width; + root.interaction(posBinding.value); + } + onPressed: e => { + widthBehavior.enabled = false; + pressStartX = e.x; + pressStartPos = root.visualPosition; + } + onReleased: e => { + root.interaction(posBinding.value); + widthBehavior.enabled = true; + dragMovement = 0; } } } diff --git a/Components/StateLayer.qml b/Components/StateLayer.qml index 53dc9e3..aa41297 100644 --- a/Components/StateLayer.qml +++ b/Components/StateLayer.qml @@ -1,15 +1,53 @@ -import qs.Config import QtQuick +import QtQuick.Shapes +import ZShell +import ZShell.Components +import qs.Helpers +import qs.Config MouseArea { id: root - property color color: DynamicColors.palette.m3onSurface + property alias bottomLeftRadius: base.bottomLeftRadius + property alias bottomRightRadius: base.bottomRightRadius + property real circleRadius + property alias color: base.color property bool disabled - property real radius: parent?.radius ?? 0 - property alias rect: hoverLayer + readonly property real endRadius: { + const d1 = distSq(0, 0); + const d2 = distSq(width, 0); + const d3 = distSq(0, height); + const d4 = distSq(width, height); + return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3; + } + property real endRadiusAtPress + property bool manualPressOverride + property real pressX: width / 2 + property real pressY: height / 2 + property alias radius: base.radius + readonly property alias rect: base + property bool shapeMorph + property bool showHoverBackground: true + property real stateOpacity: containsMouse ? 0.08 : 0 + property alias topLeftRadius: base.topLeftRadius + property alias topRightRadius: base.topRightRadius - function onClicked(): void { + function clamp(r: real): real { + return Math.max(0, Math.min(r, width / 2, height / 2)); + } + + function distSq(x: real, y: real): real { + return (pressX - x) ** 2 + (pressY - y) ** 2; + } + + function press(x: real, y: real): void { + pressX = x; + pressY = y; + fadeAnim.complete(); + circleRadius = 0; + circle.opacity = 0.1; + rippleAnim.restart(); + endRadiusAtPress = endRadius; } anchors.fill: parent @@ -17,79 +55,146 @@ MouseArea { enabled: !disabled hoverEnabled: true - onClicked: event => !disabled && onClicked(event) - onPressed: event => { - if (disabled) - return; - - rippleAnim.x = event.x; - rippleAnim.y = event.y; - - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); - - rippleAnim.restart(); + Behavior on stateOpacity { + Anim { + type: Anim.DefaultEffects + } } - SequentialAnimation { + onCircleRadiusChanged: { + if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) + fadeAnim.start(); + } + onClicked: event => !disabled && onClicked(event) + onManualPressOverrideChanged: { + if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) + fadeAnim.start(); + } + onPressed: e => press(e.x, e.y) + onPressedChanged: { + if (!(pressed || manualPressOverride) && !rippleAnim.running && circle.opacity > 0) + fadeAnim.start(); + } + + Anim { id: rippleAnim - property real radius - property real x - property real y - - PropertyAction { - property: "x" - target: ripple - value: rippleAnim.x - } - - PropertyAction { - property: "y" - target: ripple - value: rippleAnim.y - } - - PropertyAction { - property: "opacity" - target: ripple - value: 0.08 - } - - Anim { - easing.bezierCurve: MaterialEasing.standardDecel - from: 0 - properties: "implicitWidth,implicitHeight" - target: ripple - to: rippleAnim.radius * 2 - } - - Anim { - property: "opacity" - target: ripple - to: 0 - } + alwaysRunToEnd: true + duration: Appearance.anim.durations.expressiveSlowEffects * 2 + easing.bezierCurve: Appearance.anim.curves.standard + property: "circleRadius" + target: root + to: root.endRadius } - CustomClippingRect { - id: hoverLayer + Anim { + id: fadeAnim + + property: "opacity" + target: circle + to: 0 + type: Anim.SlowEffects + } + + CustomRect { + id: base anchors.fill: parent - border.pixelAligned: false - color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0) - radius: root.radius + bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0 + bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0 + color: DynamicColors.palette.m3onSurface + opacity: root.stateOpacity + // Pick up radius from parent if it has one (parent can be anything with radius props) + // qmllint disable missing-property + radius: root.parent?.radius ?? 0 + topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0 + topRightRadius: root.parent?.topRightRadius ?? radius ?? 0 + // qmllint enable missing-property + } - CustomRect { - id: ripple + Shape { + id: circle - border.pixelAligned: false - color: root.color - opacity: 0 - radius: Appearance.rounding.full + anchors.fill: parent + opacity: 0 + preferredRendererType: Shape.CurveRenderer - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 + ShapePath { + fillColor: base.color + startX: root.clamp(base.topLeftRadius) + startY: 0 + strokeColor: "transparent" + strokeWidth: 0 + + fillGradient: RadialGradient { + centerRadius: root.circleRadius + centerX: root.pressX + centerY: root.pressY + focalX: centerX + focalY: centerY + + GradientStop { + color: Qt.alpha(base.color, 1) + position: 0 + } + + GradientStop { + color: Qt.alpha(base.color, 1) + position: ZUtils.clamp(1 - 0.2 * root.endRadius / root.circleRadius, 0.01, 0.99) + } + + GradientStop { + color: Qt.alpha(base.color, ZUtils.clamp((root.circleRadius / root.endRadius - 0.9) / 0.1, 0, 1)) + position: 1 + } + } + + PathLine { + x: root.width - root.clamp(base.topRightRadius) + y: 0 + } + + PathArc { + radiusX: root.clamp(base.topRightRadius) + radiusY: root.clamp(base.topRightRadius) + relativeX: root.clamp(base.topRightRadius) + relativeY: root.clamp(base.topRightRadius) + } + + PathLine { + x: root.width + y: root.height - root.clamp(base.bottomRightRadius) + } + + PathArc { + radiusX: root.clamp(base.bottomRightRadius) + radiusY: root.clamp(base.bottomRightRadius) + relativeX: -root.clamp(base.bottomRightRadius) + relativeY: root.clamp(base.bottomRightRadius) + } + + PathLine { + x: root.clamp(base.bottomLeftRadius) + y: root.height + } + + PathArc { + radiusX: root.clamp(base.bottomLeftRadius) + radiusY: root.clamp(base.bottomLeftRadius) + relativeX: -root.clamp(base.bottomLeftRadius) + relativeY: -root.clamp(base.bottomLeftRadius) + } + + PathLine { + x: 0 + y: root.clamp(base.topLeftRadius) + } + + PathArc { + radiusX: root.clamp(base.topLeftRadius) + radiusY: root.clamp(base.topLeftRadius) + x: root.clamp(base.topLeftRadius) + y: 0 } } } diff --git a/Config/Appearance.qml b/Config/Appearance.qml index f78ffc1..7be277e 100644 --- a/Config/Appearance.qml +++ b/Config/Appearance.qml @@ -7,8 +7,6 @@ Singleton { readonly property AppearanceConf.Deform deform: Config.appearance.deform readonly property AppearanceConf.FontStuff font: Config.appearance.font readonly property AppearanceConf.Padding padding: Config.appearance.padding - // Literally just here to shorten accessing stuff :woe: - // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Conf.appearance.xxx` readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index 60bab0f..7d3b375 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -28,9 +28,13 @@ JsonObject { property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + property list expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1] property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + property list expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1] property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + property list expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1] + property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] property list standard: [0.2, 0, 0, 1, 1, 1] property list standardAccel: [0.3, 0, 1, 1, 1, 1] property list standardDecel: [0, 0, 0, 1, 1, 1] @@ -38,7 +42,9 @@ JsonObject { component AnimDurations: JsonObject { property int expressiveDefaultSpatial: 500 * scale property int expressiveEffects: 200 * scale + property int expressiveFastEffects: 150 * scale property int expressiveFastSpatial: 350 * scale + property int expressiveSlowEffects: 300 * scale property int extraLarge: 1000 * scale property int large: 600 * scale property int normal: 400 * scale @@ -71,7 +77,7 @@ JsonObject { } component Padding: JsonObject { property int large: 15 * scale - property int larger: 13 * scale + property int larger: 12 * scale property int normal: 9 * scale property real scale: 1 property int small: 5 * scale @@ -79,6 +85,7 @@ JsonObject { property int smallest: 2 * scale } component Rounding: JsonObject { + property int extraSmall: 4 * scale property int full: 1000 * scale property int large: 24 * scale property int normal: 18 * scale @@ -87,6 +94,7 @@ JsonObject { property int smallest: 8 * scale } component Spacing: JsonObject { + property int extraSmall: 4 * scale property int large: 20 * scale property int larger: 16 * scale property int normal: 12 * scale diff --git a/Modules/Settings/Controls/WallpaperCropper.qml b/Modules/Settings/Controls/WallpaperCropper.qml index f3da6f1..ff90251 100644 --- a/Modules/Settings/Controls/WallpaperCropper.qml +++ b/Modules/Settings/Controls/WallpaperCropper.qml @@ -141,14 +141,14 @@ Item { id: zoomSlider Layout.fillWidth: true - Layout.preferredHeight: 30 + Layout.preferredHeight: Appearance.padding.larger * 3 from: 1.0 - implicitHeight: 30 + implicitHeight: Appearance.padding.larger * 3 to: 5.0 value: cropRectLoader.item ? cropRectLoader.item.zoom : 1.0 - onMoved: { - delegate.zoomClipRect(value); + onInteraction: value => { + delegate.zoomClipRect(1 + (value * 4)); wrapper.changesMade = true; } } diff --git a/Modules/SysTray/Popouts/AudioPopup.qml b/Modules/SysTray/Popouts/AudioPopup.qml index 9d2d92b..e6b8ca8 100644 --- a/Modules/SysTray/Popouts/AudioPopup.qml +++ b/Modules/SysTray/Popouts/AudioPopup.qml @@ -19,7 +19,7 @@ Item { required property var wrapper implicitHeight: vol.implicitHeight + Appearance.padding.small * 2 - implicitWidth: 400 + Appearance.padding.small * 2 + implicitWidth: 600 + Appearance.padding.small * 2 CustomRect { anchors.left: parent.left @@ -52,7 +52,7 @@ Item { CustomRect { Layout.fillWidth: true - Layout.preferredHeight: 50 + Appearance.spacing.smaller * 2 + Layout.preferredHeight: 65 + Appearance.spacing.smaller * 2 Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3surfaceContainer radius: root.rounding @@ -62,15 +62,15 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.larger anchors.top: parent.top implicitWidth: childrenRect.width CustomRect { anchors.centerIn: parent color: Audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary - implicitHeight: 40 - implicitWidth: 40 + implicitHeight: 54 + implicitWidth: 54 radius: Appearance.rounding.full MaterialIcon { @@ -78,7 +78,7 @@ Item { anchors.centerIn: parent animate: true color: Audio.muted ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onPrimary - font.pointSize: 22 + font.pointSize: Appearance.font.size.extraLarge text: Audio.muted ? "volume_off" : "volume_up" } @@ -98,7 +98,7 @@ Item { anchors.bottom: parent.bottom anchors.bottomMargin: Appearance.padding.smallest anchors.left: sinkIcon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Appearance.spacing.larger anchors.right: parent.right anchors.rightMargin: Appearance.padding.large anchors.top: parent.top @@ -122,14 +122,14 @@ Item { } CustomMouseArea { - Layout.bottomMargin: 5 - Layout.fillHeight: true + Layout.bottomMargin: Appearance.padding.normal Layout.fillWidth: true + Layout.preferredHeight: Appearance.padding.larger * 3 CustomSlider { anchors.left: parent.left anchors.right: parent.right - color: Audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary + fgColor: Audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary implicitHeight: parent.height value: Audio.volume @@ -138,7 +138,7 @@ Item { } } - onMoved: Audio.setVolume(value) + onInteraction: value => Audio.setVolume(value) } } } @@ -146,7 +146,7 @@ Item { CustomClippingRect { Layout.fillWidth: true - Layout.preferredHeight: 50 + Appearance.spacing.smaller * 2 + Layout.preferredHeight: 65 + Appearance.spacing.smaller * 2 Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3surfaceContainer radius: root.rounding @@ -178,15 +178,15 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.larger anchors.top: parent.top implicitWidth: childrenRect.width CustomRect { anchors.centerIn: parent color: Audio.sourceMuted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary - implicitHeight: 40 - implicitWidth: 40 + implicitHeight: 54 + implicitWidth: 54 radius: Appearance.rounding.full MaterialIcon { @@ -194,7 +194,7 @@ Item { anchors.centerIn: parent animate: true color: Audio.sourceMuted ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onPrimary - font.pointSize: 22 + font.pointSize: Appearance.font.size.extraLarge text: Audio.sourceMuted ? "mic_off" : "mic" } @@ -214,7 +214,7 @@ Item { anchors.bottom: parent.bottom anchors.bottomMargin: Appearance.padding.smallest anchors.left: sourceIcon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Appearance.spacing.larger anchors.right: parent.right anchors.rightMargin: Appearance.padding.large anchors.top: parent.top @@ -238,14 +238,14 @@ Item { } CustomMouseArea { - Layout.bottomMargin: 5 - Layout.fillHeight: true + Layout.bottomMargin: Appearance.padding.normal Layout.fillWidth: true + Layout.preferredHeight: Appearance.padding.larger * 3 CustomSlider { anchors.left: parent.left anchors.right: parent.right - color: Audio.sourceMuted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary + fgColor: Audio.sourceMuted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary implicitHeight: parent.height value: Audio.sourceVolume @@ -254,7 +254,7 @@ Item { } } - onMoved: Audio.setSourceVolume(value) + onInteraction: value => Audio.setSourceVolume(value) } } } @@ -280,7 +280,7 @@ Item { required property var modelData Layout.fillWidth: true - Layout.preferredHeight: 50 + Appearance.spacing.smaller * 2 + Layout.preferredHeight: 65 + Appearance.spacing.smaller * 2 Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3surfaceContainer radius: root.rounding @@ -312,15 +312,15 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.larger anchors.top: parent.top implicitWidth: childrenRect.width CustomRect { anchors.centerIn: parent color: appBox.modelData.audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary - implicitHeight: 40 - implicitWidth: 40 + implicitHeight: 54 + implicitWidth: 54 radius: Appearance.rounding.full MaterialIcon { @@ -329,7 +329,7 @@ Item { anchors.centerIn: parent animate: true color: appBox.modelData.audio.muted ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onPrimary - font.pointSize: 22 + font.pointSize: Appearance.font.size.extraLarge text: appBox.modelData.audio.muted ? "volume_off" : "volume_up" } @@ -356,7 +356,7 @@ Item { anchors.bottom: parent.bottom anchors.bottomMargin: Appearance.padding.smallest anchors.left: appBoxIcon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Appearance.spacing.larger anchors.right: parent.right anchors.rightMargin: Appearance.padding.large anchors.top: parent.top @@ -381,18 +381,18 @@ Item { } CustomMouseArea { - Layout.bottomMargin: 5 - Layout.fillHeight: true + Layout.bottomMargin: Appearance.padding.normal Layout.fillWidth: true + Layout.preferredHeight: Appearance.padding.larger * 3 CustomSlider { anchors.left: parent.left anchors.right: parent.right - color: appBox.modelData.audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary + fgColor: appBox.modelData.audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary implicitHeight: parent.height value: appBox.modelData.audio.volume - onMoved: { + onInteraction: value => { Audio.setStreamVolume(appBox.modelData, value); } } diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index f691bc9..0217b06 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -45,6 +45,7 @@ qml_module(ZShell requests.hpp requests.cpp toaster.hpp toaster.cpp qalculator.hpp qalculator.cpp + zutils.hpp zutils.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/Plugins/ZShell/Components/CMakeLists.txt b/Plugins/ZShell/Components/CMakeLists.txt index f34e7fb..16d7aa3 100644 --- a/Plugins/ZShell/Components/CMakeLists.txt +++ b/Plugins/ZShell/Components/CMakeLists.txt @@ -2,6 +2,7 @@ qml_module(ZShell-components URI ZShell.Components SOURCES lazylistview.hpp lazylistview.cpp + wavyline.hpp wavyline.cpp LIBRARIES Qt::Quick ) diff --git a/Plugins/ZShell/Components/wavyline.cpp b/Plugins/ZShell/Components/wavyline.cpp new file mode 100644 index 0000000..757b7a2 --- /dev/null +++ b/Plugins/ZShell/Components/wavyline.cpp @@ -0,0 +1,255 @@ +#include "wavyline.hpp" + +#include +#include + +namespace ZShell::controls { + +WavyLine::WavyLine(QQuickItem* parent) + : QQuickPaintedItem(parent) + , m_lineWidth(4) + , m_amplitudeMultiplier(0.5) + , m_frequency(6) + , m_startX(0) + , m_fullLength(0) + , m_color(Qt::white) + , m_waveProgress(0) + , m_pathType(Linear) + , m_startAngle(0) + , m_fullAngle(360) + , m_radius(-1) + , m_value(1) + , m_startAngleRad(0) + , m_fullAngleRad(2 * M_PI) { + setAntialiasing(true); +} + +int WavyLine::lineWidth() const { + return m_lineWidth; +} + +void WavyLine::setLineWidth(int lineWidth) { + if (m_lineWidth != lineWidth) { + m_lineWidth = lineWidth; + emit lineWidthChanged(); + update(); + } +} + +qreal WavyLine::amplitudeMultiplier() const { + return m_amplitudeMultiplier; +} + +void WavyLine::setAmplitudeMultiplier(qreal amplitudeMultiplier) { + if (!qFuzzyCompare(m_amplitudeMultiplier + 1.0, amplitudeMultiplier + 1.0)) { + m_amplitudeMultiplier = amplitudeMultiplier; + emit amplitudeMultiplierChanged(); + update(); + } +} + +int WavyLine::frequency() const { + return m_frequency; +} + +void WavyLine::setFrequency(int frequency) { + if (m_frequency != frequency) { + m_frequency = frequency; + emit frequencyChanged(); + update(); + } +} + +qreal WavyLine::startX() const { + return m_startX; +} + +void WavyLine::setStartX(qreal startX) { + if (!qFuzzyCompare(m_startX + 1.0, startX + 1.0)) { + m_startX = startX; + emit startXChanged(); + update(); + } +} + +qreal WavyLine::fullLength() const { + return m_fullLength; +} + +void WavyLine::setFullLength(qreal fullLength) { + if (!qFuzzyCompare(m_fullLength + 1.0, fullLength + 1.0)) { + m_fullLength = fullLength; + emit fullLengthChanged(); + update(); + } +} + +QColor WavyLine::color() const { + return m_color; +} + +void WavyLine::setColor(const QColor& color) { + if (m_color != color) { + m_color = color; + emit colorChanged(); + update(); + } +} + +qreal WavyLine::waveProgress() const { + return m_waveProgress; +} + +void WavyLine::setWaveProgress(qreal progress) { + if (!qFuzzyCompare(m_waveProgress + 1.0, progress + 1.0)) { + m_waveProgress = progress; + emit waveProgressChanged(); + update(); + } +} + +WavyLine::PathType WavyLine::pathType() const { + return m_pathType; +} + +void WavyLine::setPathType(PathType pathType) { + if (m_pathType != pathType) { + m_pathType = pathType; + emit pathTypeChanged(); + update(); + } +} + +qreal WavyLine::startAngle() const { + return m_startAngle; +} + +void WavyLine::setStartAngle(qreal startAngle) { + if (!qFuzzyCompare(m_startAngle + 1.0, startAngle + 1.0)) { + m_startAngle = startAngle; + m_startAngleRad = startAngle * M_PI / 180.0; + emit startAngleChanged(); + update(); + } +} + +qreal WavyLine::fullAngle() const { + return m_fullAngle; +} + +void WavyLine::setFullAngle(qreal fullAngle) { + if (!qFuzzyCompare(m_fullAngle + 1.0, fullAngle + 1.0)) { + m_fullAngle = fullAngle; + m_fullAngleRad = fullAngle * M_PI / 180.0; + emit fullAngleChanged(); + update(); + } +} + +qreal WavyLine::radius() const { + return m_radius; +} + +void WavyLine::setRadius(qreal radius) { + if (!qFuzzyCompare(m_radius + 1.0, radius + 1.0)) { + m_radius = radius; + emit radiusChanged(); + update(); + } +} + +qreal WavyLine::value() const { + return m_value; +} + +void WavyLine::setValue(qreal value) { + if (!qFuzzyCompare(m_value + 1.0, value + 1.0)) { + m_value = value; + emit valueChanged(); + update(); + } +} + +void WavyLine::paint(QPainter* painter) { + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(QPen(m_color, m_lineWidth, Qt::SolidLine, Qt::RoundCap)); + + if (m_pathType == Arc) { + paintArc(painter); + } else { + paintLinear(painter); + } +} + +void WavyLine::paintLinear(QPainter* painter) { + const auto amplitude = m_lineWidth * m_amplitudeMultiplier; + const auto phase = m_waveProgress * 2 * M_PI; + const auto centerY = height() / 2; + const auto len = m_fullLength > 0 ? m_fullLength : 1; + const auto start = m_lineWidth / 2.0; + const auto fullEnd = width() - m_lineWidth / 2.0; + const auto drawEnd = start + (fullEnd - start) * m_value; + + QPainterPath path; + bool first = true; + + for (int x = m_lineWidth / 2; x <= drawEnd; ++x) { + const auto theta = m_frequency * 2 * M_PI * (x + m_startX) / len + phase; + const auto waveY = centerY + amplitude * qSin(theta); + if (first) { + path.moveTo(x, waveY); + first = false; + } else { + path.lineTo(x, waveY); + } + } + + painter->drawPath(path); +} + +void WavyLine::paintArc(QPainter* painter) { + if (m_fullAngleRad <= 0) { + return; + } + + const auto amplitude = m_lineWidth * m_amplitudeMultiplier; + const auto cx = width() / 2.0; + const auto cy = height() / 2.0; + const auto radius = m_radius > 0 ? m_radius : (qMin(width(), height()) - m_lineWidth - 2 * amplitude) / 2.0; + + if (radius <= 0) { + return; + } + + const auto phase = m_waveProgress * 2 * M_PI; + const auto arcLen = radius * m_fullAngleRad; + const auto len = m_fullLength > 0 ? m_fullLength : arcLen; + const auto drawAngleRad = m_fullAngleRad * m_value; + + if (drawAngleRad <= 0) { + return; + } + + const auto N = qMax(64, qCeil(radius * drawAngleRad)); + const auto dTheta = drawAngleRad / N; + + QPainterPath path; + + for (int i = 0; i <= N; ++i) { + const auto theta = m_startAngleRad + i * dTheta; + const auto s = i * dTheta * radius; + const auto phi = m_frequency * 2 * M_PI * (s + m_startX) / len + phase; + const auto r = radius + amplitude * qSin(phi); + const auto px = cx + r * qCos(theta); + const auto py = cy + r * qSin(theta); + if (i == 0) { + path.moveTo(px, py); + } else { + path.lineTo(px, py); + } + } + + painter->drawPath(path); +} + +} // namespace ZShell::controls diff --git a/Plugins/ZShell/Components/wavyline.hpp b/Plugins/ZShell/Components/wavyline.hpp new file mode 100644 index 0000000..e9fab48 --- /dev/null +++ b/Plugins/ZShell/Components/wavyline.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +namespace ZShell::controls { + +class WavyLine : public QQuickPaintedItem { +Q_OBJECT +QML_ELEMENT + +Q_PROPERTY(int lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged FINAL) +Q_PROPERTY(qreal amplitudeMultiplier READ amplitudeMultiplier WRITE setAmplitudeMultiplier NOTIFY + amplitudeMultiplierChanged FINAL) +Q_PROPERTY(int frequency READ frequency WRITE setFrequency NOTIFY frequencyChanged FINAL) +Q_PROPERTY(qreal startX READ startX WRITE setStartX NOTIFY startXChanged FINAL) +Q_PROPERTY(qreal fullLength READ fullLength WRITE setFullLength NOTIFY fullLengthChanged FINAL) +Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL) +Q_PROPERTY(qreal waveProgress READ waveProgress WRITE setWaveProgress NOTIFY waveProgressChanged FINAL) +Q_PROPERTY(PathType pathType READ pathType WRITE setPathType NOTIFY pathTypeChanged FINAL) +Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged FINAL) +Q_PROPERTY(qreal fullAngle READ fullAngle WRITE setFullAngle NOTIFY fullAngleChanged FINAL) +Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged FINAL) +Q_PROPERTY(qreal value READ value WRITE setValue NOTIFY valueChanged FINAL) + +public: +enum PathType { + Linear, + Arc +}; +Q_ENUM(PathType) + +explicit WavyLine(QQuickItem* parent = nullptr); + +[[nodiscard]] int lineWidth() const; +void setLineWidth(int lineWidth); + +[[nodiscard]] qreal amplitudeMultiplier() const; +void setAmplitudeMultiplier(qreal amplitudeMultiplier); + +[[nodiscard]] int frequency() const; +void setFrequency(int frequency); + +[[nodiscard]] qreal startX() const; +void setStartX(qreal startX); + +[[nodiscard]] qreal fullLength() const; +void setFullLength(qreal fullLength); + +[[nodiscard]] QColor color() const; +void setColor(const QColor& color); + +[[nodiscard]] qreal waveProgress() const; +void setWaveProgress(qreal progress); + +[[nodiscard]] PathType pathType() const; +void setPathType(PathType pathType); + +[[nodiscard]] qreal startAngle() const; +void setStartAngle(qreal startAngle); + +[[nodiscard]] qreal fullAngle() const; +void setFullAngle(qreal fullAngle); + +[[nodiscard]] qreal radius() const; +void setRadius(qreal radius); + +[[nodiscard]] qreal value() const; +void setValue(qreal value); + +void paint(QPainter* painter) override; + +signals: +void lineWidthChanged(); +void amplitudeMultiplierChanged(); +void frequencyChanged(); +void startXChanged(); +void fullLengthChanged(); +void colorChanged(); +void waveProgressChanged(); +void pathTypeChanged(); +void startAngleChanged(); +void fullAngleChanged(); +void radiusChanged(); +void valueChanged(); + +private: +void paintLinear(QPainter* painter); +void paintArc(QPainter* painter); + +int m_lineWidth; +qreal m_amplitudeMultiplier; +int m_frequency; +qreal m_startX; +qreal m_fullLength; +QColor m_color; +qreal m_waveProgress; +PathType m_pathType; +qreal m_startAngle; +qreal m_fullAngle; +qreal m_radius; +qreal m_value; +qreal m_startAngleRad; +qreal m_fullAngleRad; +}; + +} // namespace ZShell::controls diff --git a/Plugins/ZShell/zutils.cpp b/Plugins/ZShell/zutils.cpp new file mode 100644 index 0000000..c26bd0f --- /dev/null +++ b/Plugins/ZShell/zutils.cpp @@ -0,0 +1,145 @@ +#include "zutils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcZUtils, "ZShell.cutils", QtInfoMsg) + +namespace ZShell { + +void ZUtils::saveItem(QQuickItem* target, const QUrl& path) { + this->saveItem(target, path, QRect(), QJSValue(), QJSValue()); +} + +void ZUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) { + this->saveItem(target, path, rect, QJSValue(), QJSValue()); +} + +void ZUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) { + this->saveItem(target, path, QRect(), onSaved, QJSValue()); +} + +void ZUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) { + this->saveItem(target, path, QRect(), onSaved, onFailed); +} + +void ZUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) { + this->saveItem(target, path, rect, onSaved, QJSValue()); +} + +void ZUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { + if (!target) { + qCWarning(lcZUtils) << "saveItem: a target is required"; + return; + } + + if (!path.isLocalFile()) { + qCWarning(lcZUtils) << "saveItem:" << path << "is not a local file"; + return; + } + + if (!target->window()) { + qCWarning(lcZUtils) << "saveItem: unable to save target" << target << "without a window"; + return; + } + + auto scaledRect = rect; + const qreal scale = target->window()->devicePixelRatio(); + if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) { + scaledRect = + QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect(); + } + + const QSharedPointer grabResult = target->grabToImage(); + + QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, + [grabResult, scaledRect, path, onSaved, onFailed, this]() { + const auto future = QtConcurrent::run([=]() { + QImage image = grabResult->image(); + + if (scaledRect.isValid()) { + image = image.copy(scaledRect); + } + + const QString file = path.toLocalFile(); + const QString parent = QFileInfo(file).absolutePath(); + return QDir().mkpath(parent) && image.save(file); + }); + + auto* watcher = new QFutureWatcher(this); + auto* engine = qmlEngine(this); + + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { + if (watcher->result()) { + if (onSaved.isCallable()) { + QJSValueList args = { QJSValue(path.toLocalFile()) }; + if (engine) { + args << engine->toScriptValue(QVariant::fromValue(path)); + } + onSaved.call(args); + } + } else { + qCWarning(lcZUtils) << "saveItem: failed to save" << path; + if (onFailed.isCallable()) { + if (engine) { + onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); + } else { + onFailed.call(); + } + } + } + watcher->deleteLater(); + }); + watcher->setFuture(future); + }); +} + +bool ZUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) { + if (!source.isLocalFile()) { + qCWarning(lcZUtils) << "copyFile: source" << source << "is not a local file"; + return false; + } + if (!target.isLocalFile()) { + qCWarning(lcZUtils) << "copyFile: target" << target << "is not a local file"; + return false; + } + + if (overwrite && QFile::exists(target.toLocalFile())) { + if (!QFile::remove(target.toLocalFile())) { + qCWarning(lcZUtils) << "copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); + return false; + } + } + + return QFile::copy(source.toLocalFile(), target.toLocalFile()); +} + +bool ZUtils::deleteFile(const QUrl& path) { + if (!path.isLocalFile()) { + qCWarning(lcZUtils) << "deleteFile: path" << path << "is not a local file"; + return false; + } + + return QFile::remove(path.toLocalFile()); +} + +QString ZUtils::toLocalFile(const QUrl& url) { + if (!url.isLocalFile()) { + qCWarning(lcZUtils) << "toLocalFile: given url is not a local file" << url; + return QString(); + } + + return url.toLocalFile(); +} + +qreal ZUtils::clamp(qreal value, qreal min, qreal max) { + return qBound(min, value, max); +} + +} // namespace ZShell diff --git a/Plugins/ZShell/zutils.hpp b/Plugins/ZShell/zutils.hpp new file mode 100644 index 0000000..fdc413d --- /dev/null +++ b/Plugins/ZShell/zutils.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace ZShell { + +class ZUtils : public QObject { +Q_OBJECT +QML_ELEMENT +QML_SINGLETON + +public: +// clang-format off +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); +// clang-format on + +Q_INVOKABLE static bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true); +Q_INVOKABLE static bool deleteFile(const QUrl& path); +Q_INVOKABLE static QString toLocalFile(const QUrl& url); + +Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max); +}; + +} // namespace ZShell