MaterialIcon opsz clamp, UPower popout and widget changes, improved SysTray hover detection logic
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 18s
Python / test (pull_request) Successful in 32s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m13s

This commit is contained in:
2026-06-14 12:47:20 +02:00
parent 6034c4816d
commit 3f5cc2de07
15 changed files with 221 additions and 94 deletions
+51
View File
@@ -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;
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ NumberAnimation {
return Appearance.anim.durations.expressiveSlowEffects; return Appearance.anim.durations.expressiveSlowEffects;
const types = ["small", "normal", "large", "extraLarge"]; 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]]; return Appearance.anim.durations[types[idx]];
} }
easing.bezierCurve: { easing.bezierCurve: {
+6 -10
View File
@@ -28,25 +28,21 @@ Text {
SequentialAnimation { SequentialAnimation {
Anim { Anim {
easing.bezierCurve: MaterialEasing.standardAccel property: root.animateProp
target: root
to: root.animateFrom to: root.animateFrom
type: Anim.FastEffects
} }
PropertyAction { PropertyAction {
} }
Anim { Anim {
easing.bezierCurve: MaterialEasing.standardDecel property: root.animateProp
target: root
to: root.animateTo 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
}
} }
+14 -3
View File
@@ -1,15 +1,26 @@
import qs.Config import qs.Config
CustomText { CustomText {
property real fill id: root
readonly property list<int> allowed: [20, 24, 40, 48]
property real fill: 0
property int grade: DynamicColors.light ? 0 : -25 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.family: "Material Symbols Rounded"
font.pointSize: Appearance.font.size.larger font.pointSize: Appearance.font.size.larger
font.variableAxes: ({ font.variableAxes: ({
FILL: fill.toFixed(1), FILL: fill,
GRAD: grade, GRAD: grade,
opsz: fontInfo.pixelSize, opsz: opsz,
wght: fontInfo.weight wght: fontInfo.weight
}) })
} }
+1 -1
View File
@@ -254,7 +254,7 @@ CustomWindow {
implicitHeight: panels.popouts.height * (1 + extraHeight) implicitHeight: panels.popouts.height * (1 + extraHeight)
implicitWidth: panels.popouts.width implicitWidth: panels.popouts.width
panel: panels.popoutsWrapper 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 x: panels.popoutsWrapper.x + panels.popouts.x + Config.barConfig.border
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
+2
View File
@@ -29,4 +29,6 @@ Singleton {
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
readonly property bool onBattery: UPower.onBattery readonly property bool onBattery: UPower.onBattery
readonly property bool ready: UPower.displayDevice.ready readonly property bool ready: UPower.displayDevice.ready
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
readonly property real timeToFull: UPower.displayDevice.timeToFull
} }
+3 -4
View File
@@ -17,8 +17,8 @@ Item {
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
required property PopoutState popouts required property PopoutState popouts
implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.small * 2
implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.small * 2
Item { Item {
id: content id: content
@@ -28,7 +28,7 @@ Item {
Popout { Popout {
name: "audio" name: "audio"
sourceComponent: AudioPopup { sourceComponent: AudioPopout {
wrapper: root.wrapper wrapper: root.wrapper
} }
} }
@@ -73,7 +73,6 @@ Item {
name: "upower" name: "upower"
sourceComponent: UPowerPopout { sourceComponent: UPowerPopout {
wrapper: root.popouts
} }
} }
+3 -1
View File
@@ -13,6 +13,8 @@ StackView {
id: root id: root
property int biggestWidth: 0 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 required property PopoutState popouts
property int rootWidth: 0 property int rootWidth: 0
required property QsMenuHandle trayItem required property QsMenuHandle trayItem
@@ -100,7 +102,7 @@ StackView {
asynchronous: true asynchronous: true
sourceComponent: Item { sourceComponent: Item {
implicitHeight: 30 implicitHeight: root.itemHeight
StateLayer { StateLayer {
enabled: item.modelData.enabled enabled: item.modelData.enabled
+101 -57
View File
@@ -3,16 +3,97 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell.Services.UPower import Quickshell.Services.UPower
import qs.Config import qs.Config
import qs.Helpers
import qs.Components import qs.Components
import qs.Modules
Item { Column {
id: root id: root
required property var wrapper readonly property int panelRadius: ((profiles.height / 2) + Appearance.padding.small) * Appearance.rounding.scale
implicitHeight: profiles.implicitHeight spacing: Appearance.spacing.normal
implicitWidth: profiles.implicitWidth
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 { CustomRect {
id: profiles id: profiles
@@ -28,10 +109,9 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 5 * 2 + saverLabel.contentHeight implicitHeight: indicator.height + Appearance.padding.extraSmall * 2
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 8 * 2 + saverLabel.contentWidth implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.larger * 2 + Appearance.spacing.small * 8
// color: "transparent" radius: Appearance.rounding.full
radius: (20 - Appearance.padding.small) * Appearance.rounding.scale
CustomRect { CustomRect {
id: indicator id: indicator
@@ -64,10 +144,7 @@ Item {
} }
] ]
transitions: Transition { transitions: Transition {
AnchorAnimation { AnchorAnim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.type: Easing.BezierSpline
} }
} }
} }
@@ -76,62 +153,28 @@ Item {
id: saver id: saver
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 25 anchors.leftMargin: Appearance.padding.extraSmall
anchors.top: parent.top anchors.verticalCenter: parent.verticalCenter
anchors.topMargin: 8 icon: "energy_savings_leaf"
icon: "nest_eco_leaf"
profile: PowerProfile.PowerSaver profile: PowerProfile.PowerSaver
text: "Power Saver"
}
CustomText {
id: saverLabel
anchors.horizontalCenter: saver.horizontalCenter
anchors.top: saver.bottom
font.bold: true
text: saver.text
} }
Profile { Profile {
id: balance id: balance
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
anchors.top: parent.top icon: "balance"
anchors.topMargin: 8
icon: "power_settings_new"
profile: PowerProfile.Balanced profile: PowerProfile.Balanced
text: "Balanced"
}
CustomText {
id: balanceLabel
anchors.horizontalCenter: balance.horizontalCenter
anchors.top: balance.bottom
font.bold: true
text: balance.text
} }
Profile { Profile {
id: perf id: perf
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 25 anchors.rightMargin: Appearance.padding.extraSmall
anchors.top: parent.top anchors.verticalCenter: parent.verticalCenter
anchors.topMargin: 8
icon: "bolt" icon: "bolt"
profile: PowerProfile.Performance 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 { component Profile: Item {
required property string icon required property string icon
required property int profile required property int profile
required property string text
implicitHeight: icon.implicitHeight + 5 * 2 implicitHeight: icon.implicitHeight + Appearance.padding.small
implicitWidth: icon.implicitHeight + 5 * 2 implicitWidth: icon.implicitHeight + Appearance.padding.small
StateLayer { StateLayer {
color: profiles.current === parent.icon ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface 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 color: profiles.current === text ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
fill: profiles.current === text ? 1 : 0 fill: profiles.current === text ? 1 : 0
font.pointSize: Appearance.font.size.large * 2 font.pointSize: Appearance.font.size.large * 2
grade: DynamicColors.light ? 0 : (text === "balance" ? -25 : 0)
text: parent.icon text: parent.icon
Behavior on fill { Behavior on fill {
Anim { Anim {
type: Anim.DefaultEffects
} }
} }
} }
+27 -3
View File
@@ -2,12 +2,10 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Modules.SysTray.Widgets import qs.Modules.SysTray.Widgets
import qs.Modules.SysTray.Popouts
import qs.Modules import qs.Modules
RowLayout { RowLayout {
@@ -17,11 +15,37 @@ RowLayout {
required property RowLayout loader required property RowLayout loader
required property Wrapper popouts 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) { function getHoveredSubItem(localX, localY) {
let modPos = mapToItem(sysTrayMod, localX, localY); let modPos = mapToItem(sysTrayMod, localX, localY);
if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) { if (sysTrayMod.contains(Qt.point(modPos.x, modPos.y))) {
let modRowPos = sysTrayMod.mapToItem(sysModRow, 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) {
if (child.objectName === "audioWidget" && Config.barConfig.popouts.audio) if (child.objectName === "audioWidget" && Config.barConfig.popouts.audio)
return { return {
+2
View File
@@ -14,6 +14,8 @@ RowLayout {
property color textColor: DynamicColors.palette.m3onSurface property color textColor: DynamicColors.palette.m3onSurface
MaterialIcon { MaterialIcon {
id: mic
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true animate: true
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor
+10 -14
View File
@@ -102,20 +102,16 @@ Item {
active: !Battery.isLaptop active: !Battery.isLaptop
anchors.centerIn: parent anchors.centerIn: parent
sourceComponent: RowLayout { sourceComponent: MaterialIcon {
id: upowerIcon Layout.alignment: Qt.AlignVCenter
animate: true
MaterialIcon { fill: 1
Layout.alignment: Qt.AlignVCenter text: {
animate: true if (PowerProfiles.profile === PowerProfile.PowerSaver)
fill: 1 return "energy_savings_leaf";
text: { if (PowerProfiles.profile === PowerProfile.Performance)
if (PowerProfiles.profile === PowerProfile.PowerSaver) return "bolt";
return "nest_eco_leaf"; return "balance";
if (PowerProfiles.profile === PowerProfile.Performance)
return "bolt";
return "power_settings_new";
}
} }
} }
} }