diff --git a/Components/AnchorAnim.qml b/Components/AnchorAnim.qml new file mode 100644 index 0000000..147caf2 --- /dev/null +++ b/Components/AnchorAnim.qml @@ -0,0 +1,51 @@ +import QtQuick +import qs.Config + +AnchorAnimation { + enum Type { + StandardSmall = 0, + Standard, + StandardLarge, + StandardExtraLarge, + EmphasizedSmall, + Emphasized, + EmphasizedLarge, + EmphasizedExtraLarge, + FastSpatial, + DefaultSpatial, + SlowSpatial + } + + property int type: AnchorAnim.DefaultSpatial + + duration: { + if (type < AnchorAnim.StandardSmall || type > AnchorAnim.SlowSpatial) + return Appearance.anim.durations.expressiveDefaultSpatial; + + if (type == AnchorAnim.FastSpatial) + return Appearance.anim.durations.expressiveFastSpatial; + if (type == AnchorAnim.DefaultSpatial) + return Appearance.anim.durations.expressiveDefaultSpatial; + if (type == AnchorAnim.SlowSpatial) + return Appearance.anim.durations.large; + + const types = ["small", "normal", "large", "extraLarge"]; + const idx = type % 4; + return Appearance.anim.durations[types[idx]]; + } + easing.bezierCurve: { + if (type == AnchorAnim.FastSpatial) + return Appearance.anim.curves.expressiveFastSpatial; + if (type == AnchorAnim.DefaultSpatial) + return Appearance.anim.curves.expressiveDefaultSpatial; + if (type == AnchorAnim.SlowSpatial) + return Appearance.anim.curves.expressiveSlowSpatial; + + if (type >= AnchorAnim.StandardSmall && type <= AnchorAnim.StandardExtraLarge) + return Appearance.anim.curves.standard; + if (type >= AnchorAnim.EmphasizedSmall && type <= AnchorAnim.EmphasizedExtraLarge) + return Appearance.anim.curves.emphasized; + + return Appearance.anim.curves.expressiveDefaultSpatial; + } +} diff --git a/Components/Anim.qml b/Components/Anim.qml index 0b02612..6dc33c6 100644 --- a/Components/Anim.qml +++ b/Components/Anim.qml @@ -39,7 +39,7 @@ NumberAnimation { return Appearance.anim.durations.expressiveSlowEffects; const types = ["small", "normal", "large", "extraLarge"]; - const idx = type % 4; // 0-7 are the 4 standard types + const idx = type % 4; return Appearance.anim.durations[types[idx]]; } easing.bezierCurve: { diff --git a/Components/CustomText.qml b/Components/CustomText.qml index f4c1600..1ba8898 100644 --- a/Components/CustomText.qml +++ b/Components/CustomText.qml @@ -28,25 +28,21 @@ Text { SequentialAnimation { Anim { - easing.bezierCurve: MaterialEasing.standardAccel + property: root.animateProp + target: root to: root.animateFrom + type: Anim.FastEffects } PropertyAction { } Anim { - easing.bezierCurve: MaterialEasing.standardDecel + property: root.animateProp + target: root to: root.animateTo + type: Anim.DefaultEffects } } } - - component Anim: NumberAnimation { - duration: root.animateDuration / 2 - easing.type: Easing.BezierSpline - properties: root.animateProp.split(",").length > 1 ? root.animateProp : "" - property: root.animateProp.split(",").length === 1 ? root.animateProp : "" - target: root - } } diff --git a/Components/MaterialIcon.qml b/Components/MaterialIcon.qml index 69da4e3..23df7f9 100644 --- a/Components/MaterialIcon.qml +++ b/Components/MaterialIcon.qml @@ -1,15 +1,26 @@ import qs.Config CustomText { - property real fill + id: root + + readonly property list allowed: [20, 24, 40, 48] + property real fill: 0 property int grade: DynamicColors.light ? 0 : -25 + readonly property int opsz: clampOpsz(optical) + property int optical: fontInfo.pixelSize + + function clampOpsz(value): int { + return allowed.reduce((closest, current) => { + return Math.abs(current - value) < Math.abs(closest - value) ? current : closest; + }); + } font.family: "Material Symbols Rounded" font.pointSize: Appearance.font.size.larger font.variableAxes: ({ - FILL: fill.toFixed(1), + FILL: fill, GRAD: grade, - opsz: fontInfo.pixelSize, + opsz: opsz, wght: fontInfo.weight }) } diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index c08ff63..e0ffa69 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -254,7 +254,7 @@ CustomWindow { implicitHeight: panels.popouts.height * (1 + extraHeight) implicitWidth: panels.popouts.width panel: panels.popoutsWrapper - radius: (panels.popouts.currentName.startsWith("audio") || panels.popouts.currentName.startsWith("updates")) ? Appearance.rounding.normal : 20 * Appearance.rounding.scale + radius: panels.popouts.current?.panelRadius ?? Appearance.rounding.normal x: panels.popoutsWrapper.x + panels.popouts.x + Config.barConfig.border y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight diff --git a/Helpers/Battery.qml b/Helpers/Battery.qml index 2360386..1ffb61f 100644 --- a/Helpers/Battery.qml +++ b/Helpers/Battery.qml @@ -29,4 +29,6 @@ Singleton { readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery readonly property bool onBattery: UPower.onBattery readonly property bool ready: UPower.displayDevice.ready + readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty + readonly property real timeToFull: UPower.displayDevice.timeToFull } diff --git a/Modules/Content.qml b/Modules/Content.qml index f3035d3..f48932c 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -17,8 +17,8 @@ Item { readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null required property PopoutState popouts - implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 - implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 + implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.small * 2 + implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.small * 2 Item { id: content @@ -28,7 +28,7 @@ Item { Popout { name: "audio" - sourceComponent: AudioPopup { + sourceComponent: AudioPopout { wrapper: root.wrapper } } @@ -73,7 +73,6 @@ Item { name: "upower" sourceComponent: UPowerPopout { - wrapper: root.popouts } } diff --git a/Modules/SysTray/Popouts/AudioPopup.qml b/Modules/SysTray/Popouts/AudioPopout.qml similarity index 100% rename from Modules/SysTray/Popouts/AudioPopup.qml rename to Modules/SysTray/Popouts/AudioPopout.qml diff --git a/Modules/SysTray/Popouts/TrayMenuPopout.qml b/Modules/SysTray/Popouts/TrayMenuPopout.qml index ab2459b..c212261 100644 --- a/Modules/SysTray/Popouts/TrayMenuPopout.qml +++ b/Modules/SysTray/Popouts/TrayMenuPopout.qml @@ -13,6 +13,8 @@ StackView { id: root property int biggestWidth: 0 + readonly property int itemHeight: 30 + readonly property int panelRadius: ((itemHeight / 2) + Appearance.padding.small) * Appearance.rounding.scale required property PopoutState popouts property int rootWidth: 0 required property QsMenuHandle trayItem @@ -100,7 +102,7 @@ StackView { asynchronous: true sourceComponent: Item { - implicitHeight: 30 + implicitHeight: root.itemHeight StateLayer { enabled: item.modelData.enabled diff --git a/Modules/SysTray/Popouts/UPowerPopout.qml b/Modules/SysTray/Popouts/UPowerPopout.qml index 116778a..4ed3c7c 100644 --- a/Modules/SysTray/Popouts/UPowerPopout.qml +++ b/Modules/SysTray/Popouts/UPowerPopout.qml @@ -3,16 +3,97 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell.Services.UPower import qs.Config +import qs.Helpers import qs.Components -import qs.Modules -Item { +Column { id: root - required property var wrapper + readonly property int panelRadius: ((profiles.height / 2) + Appearance.padding.small) * Appearance.rounding.scale - implicitHeight: profiles.implicitHeight - implicitWidth: profiles.implicitWidth + spacing: Appearance.spacing.normal + + Loader { + active: Battery.isLaptop + + CustomText { + text: qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) + } + } + + CustomText { + function formatSeconds(s: int, fallback: string): string { + const day = Math.floor(s / 86400); + const hr = Math.floor(s / 3600) % 60; + const min = Math.floor(s / 60) % 60; + + let comps = []; + if (day > 0) + comps.push(`${day} days`); + if (hr > 0) + comps.push(`${hr} hours`); + if (min > 0) + comps.push(`${min} mins`); + + return comps.join(", ") || fallback; + } + + anchors.left: parent.left + anchors.leftMargin: Appearance.padding.normal + text: Battery.isLaptop ? qsTr("Time %1: %2").arg(Battery.onBattery ? "remaining" : "until charged").arg(Battery.onBattery ? formatSeconds(Battery.timeToEmpty, "Calculating...") : formatSeconds(Battery.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile)) + } + + Loader { + active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None + anchors.horizontalCenter: parent.horizontalCenter + asynchronous: true + height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 + + sourceComponent: CustomRect { + color: DynamicColors.palette.m3error + implicitHeight: child.implicitHeight + Appearance.padding.large + implicitWidth: child.implicitWidth + Appearance.padding.larger * 2 + radius: Appearance.rounding.large + + Column { + id: child + + anchors.centerIn: parent + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Appearance.spacing.small + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -font.pointSize / 10 + color: DynamicColors.palette.m3onError + text: "warning" + } + + CustomText { + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onError + font.family: Appearance.font.family.mono + text: qsTr("Performance Degraded") + } + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: -font.pointSize / 10 + color: DynamicColors.palette.m3onError + text: "warning" + } + } + + CustomText { + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.palette.m3onError + text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason)) + } + } + } + } CustomRect { id: profiles @@ -28,10 +109,9 @@ Item { anchors.horizontalCenter: parent.horizontalCenter color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 5 * 2 + saverLabel.contentHeight - implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 8 * 2 + saverLabel.contentWidth - // color: "transparent" - radius: (20 - Appearance.padding.small) * Appearance.rounding.scale + implicitHeight: indicator.height + Appearance.padding.extraSmall * 2 + implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.larger * 2 + Appearance.spacing.small * 8 + radius: Appearance.rounding.full CustomRect { id: indicator @@ -64,10 +144,7 @@ Item { } ] transitions: Transition { - AnchorAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - easing.type: Easing.BezierSpline + AnchorAnim { } } } @@ -76,62 +153,28 @@ Item { id: saver anchors.left: parent.left - anchors.leftMargin: 25 - anchors.top: parent.top - anchors.topMargin: 8 - icon: "nest_eco_leaf" + anchors.leftMargin: Appearance.padding.extraSmall + anchors.verticalCenter: parent.verticalCenter + icon: "energy_savings_leaf" profile: PowerProfile.PowerSaver - text: "Power Saver" - } - - CustomText { - id: saverLabel - - anchors.horizontalCenter: saver.horizontalCenter - anchors.top: saver.bottom - font.bold: true - text: saver.text } Profile { id: balance - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 8 - icon: "power_settings_new" + anchors.centerIn: parent + icon: "balance" profile: PowerProfile.Balanced - text: "Balanced" - } - - CustomText { - id: balanceLabel - - anchors.horizontalCenter: balance.horizontalCenter - anchors.top: balance.bottom - font.bold: true - text: balance.text } Profile { id: perf anchors.right: parent.right - anchors.rightMargin: 25 - anchors.top: parent.top - anchors.topMargin: 8 + anchors.rightMargin: Appearance.padding.extraSmall + anchors.verticalCenter: parent.verticalCenter icon: "bolt" profile: PowerProfile.Performance - text: "Performance" - } - - CustomText { - id: perfLabel - - anchors.horizontalCenter: perf.horizontalCenter - anchors.top: perf.bottom - font.bold: true - text: perf.text } } @@ -147,10 +190,9 @@ Item { component Profile: Item { required property string icon required property int profile - required property string text - implicitHeight: icon.implicitHeight + 5 * 2 - implicitWidth: icon.implicitHeight + 5 * 2 + implicitHeight: icon.implicitHeight + Appearance.padding.small + implicitWidth: icon.implicitHeight + Appearance.padding.small StateLayer { color: profiles.current === parent.icon ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface @@ -168,10 +210,12 @@ Item { color: profiles.current === text ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface fill: profiles.current === text ? 1 : 0 font.pointSize: Appearance.font.size.large * 2 + grade: DynamicColors.light ? 0 : (text === "balance" ? -25 : 0) text: parent.icon Behavior on fill { Anim { + type: Anim.DefaultEffects } } } diff --git a/Modules/SysTray/TrayWidget.qml b/Modules/SysTray/TrayWidget.qml index 2d3a768..b3864c5 100644 --- a/Modules/SysTray/TrayWidget.qml +++ b/Modules/SysTray/TrayWidget.qml @@ -2,12 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts -import Quickshell import Quickshell.Services.SystemTray import qs.Components import qs.Config import qs.Modules.SysTray.Widgets -import qs.Modules.SysTray.Popouts import qs.Modules RowLayout { @@ -17,11 +15,37 @@ RowLayout { required property RowLayout loader required property Wrapper popouts + function closestRowChild(row, x) { + let child = row.childAt(x, row.height / 2); + if (child) + return child; + + let closest = null; + let closestDistance = Infinity; + + for (let i = 0; i < row.children.length; ++i) { + let c = row.children[i]; + + if (!c.visible || c.width <= 0) + continue; + + let centerX = c.x + c.width / 2; + let dist = Math.abs(x - centerX); + + if (dist < closestDistance) { + closestDistance = dist; + closest = c; + } + } + + return closest; + } + function getHoveredSubItem(localX, localY) { let modPos = mapToItem(sysTrayMod, localX, localY); if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) { let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y); - let child = sysModRow.childAt(modRowPos.x, modRowPos.y); + let child = closestRowChild(sysModRow, modRowPos.x); if (child) { if (child.objectName === "audioWidget" && Config.barConfig.popouts.audio) return { diff --git a/Modules/SysTray/Widgets/AudioWidget.qml b/Modules/SysTray/Widgets/AudioWidget.qml index c124acd..202812b 100644 --- a/Modules/SysTray/Widgets/AudioWidget.qml +++ b/Modules/SysTray/Widgets/AudioWidget.qml @@ -14,6 +14,8 @@ RowLayout { property color textColor: DynamicColors.palette.m3onSurface MaterialIcon { + id: mic + Layout.alignment: Qt.AlignVCenter animate: true color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor diff --git a/Modules/SysTray/Widgets/UPowerWidget.qml b/Modules/SysTray/Widgets/UPowerWidget.qml index 6bd0587..b3d4f7e 100644 --- a/Modules/SysTray/Widgets/UPowerWidget.qml +++ b/Modules/SysTray/Widgets/UPowerWidget.qml @@ -102,20 +102,16 @@ Item { active: !Battery.isLaptop anchors.centerIn: parent - sourceComponent: RowLayout { - id: upowerIcon - - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - animate: true - fill: 1 - text: { - if (PowerProfiles.profile === PowerProfile.PowerSaver) - return "nest_eco_leaf"; - if (PowerProfiles.profile === PowerProfile.Performance) - return "bolt"; - return "power_settings_new"; - } + sourceComponent: MaterialIcon { + Layout.alignment: Qt.AlignVCenter + animate: true + fill: 1 + text: { + if (PowerProfiles.profile === PowerProfile.PowerSaver) + return "energy_savings_leaf"; + if (PowerProfiles.profile === PowerProfile.Performance) + return "bolt"; + return "balance"; } } } diff --git a/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf b/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf new file mode 100644 index 0000000..a85dcae Binary files /dev/null and b/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf differ diff --git a/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 b/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 new file mode 100644 index 0000000..5e0e17a Binary files /dev/null and b/assets/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2 differ