diff --git a/CMakeLists.txt b/CMakeLists.txt index b23f7b48..980065b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,3 +71,20 @@ if("shell" IN_LIST ENABLE_MODULES) install(FILES LICENSE DESTINATION "${INSTALL_QSCONFDIR}") endif() + +if("plugin" IN_LIST ENABLE_MODULES OR "shell" IN_LIST ENABLE_MODULES) + message(STATUS "Fetching M3Shapes module") + include(FetchContent) + FetchContent_Declare( + m3shapes_external + GIT_REPOSITORY https://github.com/soramanew/m3shapes.git + GIT_TAG main + ) + FetchContent_MakeAvailable(m3shapes_external) + message(STATUS "Done fetching M3Shapes module") + + # Fix m3shapes wrong rpath + if(TARGET m3shapesplugin) + set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN") + endif() +endif() diff --git a/assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf b/assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf new file mode 100644 index 00000000..83f272d2 Binary files /dev/null and b/assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf differ diff --git a/assets/google-sans-flex/LICENSE b/assets/google-sans-flex/LICENSE new file mode 100644 index 00000000..e1fa6e8f --- /dev/null +++ b/assets/google-sans-flex/LICENSE @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/shaders/fade.frag b/assets/shaders/fade.frag deleted file mode 100644 index a6cdf70d..00000000 --- a/assets/shaders/fade.frag +++ /dev/null @@ -1,26 +0,0 @@ -#version 440 - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float qt_Opacity; - float fadeMargin; -}; - -layout(binding = 1) uniform sampler2D source; - -void main() { - vec4 tex = texture(source, qt_TexCoord0); - float factor = 1.0; - float margin = 0.1; - - if (qt_TexCoord0.y < margin) { - factor = qt_TexCoord0.y / margin; - } else if (qt_TexCoord0.y > (1.0 - margin)) { - factor = (1.0 - qt_TexCoord0.y) / margin; - } - - fragColor = tex * factor * qt_Opacity; -} diff --git a/assets/shaders/fade.frag.qsb b/assets/shaders/fade.frag.qsb deleted file mode 100644 index 888e4c10..00000000 Binary files a/assets/shaders/fade.frag.qsb and /dev/null differ diff --git a/assets/shaders/opacitymask.frag b/assets/shaders/opacitymask.frag deleted file mode 100644 index 94a80b8a..00000000 --- a/assets/shaders/opacitymask.frag +++ /dev/null @@ -1,19 +0,0 @@ -#version 440 - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - // qt_Matrix and qt_Opacity must always be both present - // if the built-in vertex shader is used. - mat4 qt_Matrix; - float qt_Opacity; -}; - -layout(binding = 1) uniform sampler2D source; -layout(binding = 2) uniform sampler2D maskSource; - -void main() -{ - fragColor = texture(source, qt_TexCoord0.st) * (texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity; -} diff --git a/assets/shaders/opacitymask.frag.qsb b/assets/shaders/opacitymask.frag.qsb deleted file mode 100644 index 7bf97c28..00000000 Binary files a/assets/shaders/opacitymask.frag.qsb and /dev/null differ diff --git a/components/Anim.qml b/components/Anim.qml index 8bc4468a..16f8bdb5 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -13,33 +13,48 @@ NumberAnimation { EmphasizedExtraLarge, FastSpatial, DefaultSpatial, - SlowSpatial + SlowSpatial, + FastEffects, + DefaultEffects, + SlowEffects } - property int type: Anim.Standard + property int type: Anim.DefaultSpatial duration: { - if (type < Anim.StandardSmall || type > Anim.SlowSpatial) + if (type < Anim.StandardSmall || type > Anim.SlowEffects) return Tokens.anim.durations.normal; - if (type == Anim.FastSpatial) + if (type === Anim.FastSpatial) return Tokens.anim.durations.expressiveFastSpatial; - if (type == Anim.DefaultSpatial) + if (type === Anim.DefaultSpatial) return Tokens.anim.durations.expressiveDefaultSpatial; - if (type == Anim.SlowSpatial) + if (type === Anim.SlowSpatial) return Tokens.anim.durations.expressiveSlowSpatial; + if (type === Anim.FastEffects) + return Tokens.anim.durations.expressiveFastEffects; + if (type === Anim.DefaultEffects) + return Tokens.anim.durations.expressiveDefaultEffects; + if (type === Anim.SlowEffects) + return Tokens.anim.durations.expressiveSlowEffects; const types = ["small", "normal", "large", "extraLarge"]; const idx = type % 4; // 0-7 are the 4 standard types return Tokens.anim.durations[types[idx]]; } easing: { - if (type == Anim.FastSpatial) + if (type === Anim.FastSpatial) return Tokens.anim.expressiveFastSpatial; - if (type == Anim.DefaultSpatial) + if (type === Anim.DefaultSpatial) return Tokens.anim.expressiveDefaultSpatial; - if (type == Anim.SlowSpatial) + if (type === Anim.SlowSpatial) return Tokens.anim.expressiveSlowSpatial; + if (type === Anim.FastEffects) + return Tokens.anim.expressiveFastEffects; + if (type === Anim.DefaultEffects) + return Tokens.anim.expressiveDefaultEffects; + if (type === Anim.SlowEffects) + return Tokens.anim.expressiveSlowEffects; if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) return Tokens.anim.emphasized; diff --git a/components/CAnim.qml b/components/CAnim.qml index b86fcce1..17168de3 100644 --- a/components/CAnim.qml +++ b/components/CAnim.qml @@ -2,6 +2,6 @@ import QtQuick import Caelestia.Config ColorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + duration: Tokens.anim.durations.expressiveSlowEffects + easing: Tokens.anim.expressiveSlowEffects } diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index 691fd3c5..ff4061b5 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -9,22 +9,20 @@ ColumnLayout { required property string icon required property string title - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Layout.alignment: Qt.AlignHCenter MaterialIcon { Layout.alignment: Qt.AlignHCenter animate: true text: root.icon - font.pointSize: Tokens.font.size.extraLarge * 3 - font.bold: true + fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).weight(Font.Bold).build() } StyledText { Layout.alignment: Qt.AlignHCenter animate: true text: root.title - font.pointSize: Tokens.font.size.large - font.bold: true + font: Tokens.font.title.builders.medium.weight(Font.Bold).build() } } diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index d94c3a6e..957dbb0f 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -9,7 +9,7 @@ ColumnLayout { required property var deviceDetails - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: qsTr("IP Address") @@ -18,40 +18,40 @@ ColumnLayout { StyledText { text: root.deviceDetails?.ipAddress || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Subnet Mask") } StyledText { text: root.deviceDetails?.subnet || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Gateway") } StyledText { text: root.deviceDetails?.gateway || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("DNS Servers") } StyledText { text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.Wrap Layout.maximumWidth: parent.width } diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml index 739c50ba..513244ad 100644 --- a/components/MaterialIcon.qml +++ b/components/MaterialIcon.qml @@ -1,16 +1,11 @@ +import QtQuick import Caelestia.Config import qs.services StyledText { property real fill property int grade: Colours.light ? 0 : -25 + property font fontStyle: Tokens.font.icon.small - font.family: Tokens.font.family.material - font.pointSize: Tokens.font.size.larger - font.variableAxes: ({ - FILL: fill.toFixed(1), - GRAD: grade, - opsz: fontInfo.pixelSize, - wght: fontInfo.weight - }) + font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build() } diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml index 4d302509..980a7d9d 100644 --- a/components/PropertyRow.qml +++ b/components/PropertyRow.qml @@ -11,16 +11,16 @@ ColumnLayout { required property string value property bool showTopMargin: false - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { - Layout.topMargin: root.showTopMargin ? Tokens.spacing.normal : 0 + Layout.topMargin: root.showTopMargin ? Tokens.spacing.medium : 0 text: root.label } StyledText { text: root.value color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 7aa3f13f..f539fa77 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -8,13 +8,13 @@ StyledRect { id: root default property alias content: contentColumn.data - property real contentSpacing: Tokens.spacing.larger + property real contentSpacing: Tokens.spacing.large property bool alignTop: false Layout.fillWidth: true - implicitHeight: contentColumn.implicitHeight + Tokens.padding.large * 2 + implicitHeight: contentColumn.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh ColumnLayout { diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml index 09364143..23eb913c 100644 --- a/components/SectionHeader.qml +++ b/components/SectionHeader.qml @@ -13,10 +13,9 @@ ColumnLayout { spacing: 0 StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: root.title - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } StyledText { diff --git a/components/StateLayer.qml b/components/StateLayer.qml index 2ce8c500..ca177f49 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -29,7 +29,7 @@ MouseArea { 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 ? 1.16 : 1); + return Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0); } property real endRadiusAtPress @@ -85,8 +85,7 @@ MouseArea { target: circle property: "opacity" to: 0 - easing: Tokens.anim.expressiveSlowEffects - duration: Tokens.anim.durations.expressiveSlowEffects + type: Anim.SlowEffects } StyledRect { @@ -135,14 +134,14 @@ MouseArea { startY: 0 PathLine { - x: root.width - root.clamp(base.topLeftRadius) + x: root.width - root.clamp(base.topRightRadius) y: 0 } PathArc { - relativeX: root.clamp(base.topLeftRadius) - relativeY: root.clamp(base.topLeftRadius) - radiusX: root.clamp(base.topLeftRadius) - radiusY: root.clamp(base.topLeftRadius) + relativeX: root.clamp(base.topRightRadius) + relativeY: root.clamp(base.topRightRadius) + radiusX: root.clamp(base.topRightRadius) + radiusY: root.clamp(base.topRightRadius) } PathLine { x: root.width @@ -179,8 +178,7 @@ MouseArea { Behavior on stateOpacity { Anim { - easing: Tokens.anim.expressiveDefaultEffects - duration: Tokens.anim.durations.expressiveDefaultEffects + type: Anim.DefaultEffects } } } diff --git a/components/StyledText.qml b/components/StyledText.qml index d3624c58..e39e72e7 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -8,16 +8,11 @@ Text { id: root property bool animate: false - property string animateProp: "scale" - property real animateFrom: 0 - property real animateTo: 1 - property int animateDuration: Tokens.anim.durations.normal renderType: Text.NativeRendering textFormat: Text.PlainText color: Colours.palette.m3onSurface - font.family: Tokens.font.family.sans - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small Behavior on color { CAnim {} @@ -28,20 +23,18 @@ Text { SequentialAnimation { Anim { - to: root.animateFrom - easing: Tokens.anim.standardAccel + target: root + property: "opacity" + to: 0 + type: Anim.FastEffects } PropertyAction {} Anim { - to: root.animateTo - easing: Tokens.anim.standardDecel + target: root + property: "opacity" + to: 1 + type: Anim.DefaultEffects } } } - - component Anim: NumberAnimation { - target: root - property: root.animateProp - duration: root.animateDuration / 2 - } } diff --git a/components/controls/ButtonBase.qml b/components/controls/ButtonBase.qml new file mode 100644 index 00000000..4c85bc9e --- /dev/null +++ b/components/controls/ButtonBase.qml @@ -0,0 +1,97 @@ +//@ pragma Internal + +import QtQuick +import Caelestia.Config +import qs.components +import qs.services + +StyledRect { + id: root + + enum ButtonType { + Filled, + Tonal, + Text + } + + property bool checked + property alias disabled: stateLayer.disabled + property bool isToggle + property bool isRound + + property bool radiusMorph: true + property alias shapeMorph: stateLayer.shapeMorph + property bool fillWidth // For ButtonRow + + property font font + property int type: ButtonBase.Filled + + property real padding + property real horizontalPadding: padding + property real verticalPadding: padding + + readonly property alias pressed: stateLayer.pressed + readonly property alias hovered: stateLayer.containsMouse + readonly property alias stateLayer: stateLayer + readonly property alias radiusAnim: radiusAnim + + required property color activeColour + required property color inactiveColour + required property color activeOnColour + required property color inactiveOnColour + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + + property bool internalChecked + property real shapeMorphExpansion: shapeMorph && pressed ? 24 : 0 // Apparently it's always 24px no matter the width of the button + readonly property color onColour: disabled ? disabledOnColour : internalChecked ? activeOnColour : inactiveOnColour + + property real pressedRadius: Tokens.rounding.small + property real checkedRadius: Tokens.rounding.medium + property real defaultRadius: Tokens.rounding.large + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: { + if (radiusMorph && pressed) + return pressedRadius; + if (internalChecked) + return checkedRadius; + if (isRound) + return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale); + return defaultRadius; + } + color: type === ButtonBase.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + + // Make size required so we don't forget to set it + required implicitWidth + required implicitHeight + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled + onClicked: { + if (root.isToggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + Behavior on radius { + Anim { + id: radiusAnim + + type: Anim.DefaultEffects + } + } + + Behavior on shapeMorphExpansion { + Anim { + type: Anim.FastSpatial + } + } +} diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index aabda13a..7c4c8c4e 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -19,8 +19,8 @@ BusyIndicator { Completing } - property real implicitSize: Tokens.font.size.normal * 3 - property real strokeWidth: Tokens.padding.small * 0.8 + property real implicitSize: Tokens.font.body.medium.pointSize * 3 + property real strokeWidth: Tokens.padding.extraSmall property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer @@ -63,6 +63,7 @@ BusyIndicator { transitions: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,internalStrokeWidth" duration: manager.completeEndDuration * Tokens.anim.durations.scale } @@ -77,6 +78,7 @@ BusyIndicator { rotation: manager.rotation startAngle: manager.startFraction * 360 value: manager.endFraction - manager.startFraction + hasEndIndicator: false } CircularIndicatorManager { diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 2372c86c..a501e12b 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,69 +1,122 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Shapes +import Caelestia.Components import Caelestia.Config import qs.components import qs.services -Shape { +Item { id: root property real value property int startAngle: -90 - property int strokeWidth: Tokens.padding.smaller + property int sweepAngle: 360 + property int strokeWidth: Tokens.padding.small property int padding: 0 property int spacing: Tokens.spacing.small property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer + property alias hasEndIndicator: dot.active + + property bool wavy: false + property alias waveFrequency: wave.frequency + property alias waveAmplitude: wave.amplitudeMultiplier + property bool wavePaused + property alias waveDuration: waveProgAnim.duration readonly property real size: Math.min(width, height) - readonly property real arcRadius: (size - padding - strokeWidth) / 2 - readonly property real vValue: value || 1 / 360 + readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2 + property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value)) // Not readonly for animations readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) + readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180 - preferredRendererType: Shape.CurveRenderer - asynchronous: true + readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2 // For consumers + property real implicitSize - ShapePath { - fillColor: "transparent" - strokeColor: root.bgColour - strokeWidth: root.strokeWidth - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + implicitWidth: implicitSize + implicitHeight: implicitSize - PathAngleArc { - startAngle: root.startAngle + 360 * root.vValue + root.gapAngle - sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 - } + Shape { + preferredRendererType: Shape.CurveRenderer + asynchronous: true + opacity: Math.min(1, remainingArc.sweepAngle) - Behavior on strokeColor { - CAnim { - duration: Tokens.anim.durations.large + ShapePath { + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth + capStyle: ShapePath.RoundCap + + PathAngleArc { + id: remainingArc + + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle + sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * (root.sweepAngle < 360 ? 1 : 2)) + } + + Behavior on strokeColor { + CAnim {} } } } - ShapePath { - fillColor: "transparent" - strokeColor: root.fgColour - strokeWidth: root.strokeWidth - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + WavyLine { + id: wave - PathAngleArc { - startAngle: root.startAngle - sweepAngle: 360 * root.vValue - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 + anchors.fill: parent + anchors.margins: -lineWidth * amplitudeMultiplier + + lineWidth: root.strokeWidth + color: root.fgColour + pathType: WavyLine.Arc + radius: root.arcRadius + startAngle: root.startAngle + fullAngle: root.sweepAngle + value: root.clampedVal + frequency: 8 + amplitudeMultiplier: root.wavy ? 0.5 : 0 + + Anim on waveProgress { + id: waveProgAnim + + running: true + paused: root.wavePaused || wave.amplitudeMultiplier === 0 + from: 0 + to: 1 + duration: 2000 + easing.type: Easing.Linear + loops: Animation.Infinite } - Behavior on strokeColor { - CAnim { - duration: Tokens.anim.durations.large + Behavior on color { + CAnim {} + } + + Behavior on amplitudeMultiplier { + Anim { + type: Anim.DefaultEffects } } } + + Loader { + id: dot + + x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2 + y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2 + + sourceComponent: StyledRect { + radius: Tokens.rounding.full + color: root.fgColour + opacity: Math.min(1, remainingArc.sweepAngle) + implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth) + implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth) + } + } } diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 80ea8fe8..3ab10c7a 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -24,7 +24,7 @@ ColumnLayout { id: sectionHeaderItem Layout.fillWidth: true - Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.normal * 2, 48) + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.medium * 2, 48) RowLayout { id: titleRow @@ -32,14 +32,13 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.normal - anchors.rightMargin: Tokens.padding.normal - spacing: Tokens.spacing.normal + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + spacing: Tokens.spacing.medium StyledText { text: root.title - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } Item { @@ -50,7 +49,7 @@ ColumnLayout { text: "expand_more" rotation: root.expanded ? 180 : 0 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal + fontStyle: Tokens.font.icon.medium Behavior on rotation { Anim { @@ -63,7 +62,7 @@ ColumnLayout { StateLayer { anchors.fill: parent color: Colours.palette.m3onSurface - radius: Tokens.rounding.normal + radius: Tokens.rounding.large showHoverBackground: false onClicked: { root.toggleRequested(); @@ -76,7 +75,7 @@ ColumnLayout { id: contentWrapper Layout.fillWidth: true - Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.small * 2) : 0 + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.large) : 0 clip: true Behavior on Layout.preferredHeight { @@ -87,13 +86,15 @@ ColumnLayout { id: backgroundRect anchors.fill: parent - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) opacity: root.showBackground && root.expanded ? 1.0 : 0.0 visible: root.showBackground Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -103,26 +104,28 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right y: Tokens.spacing.small - anchors.leftMargin: Tokens.padding.normal - anchors.rightMargin: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium anchors.bottomMargin: Tokens.spacing.small spacing: Tokens.spacing.small opacity: root.expanded ? 1.0 : 0.0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } StyledText { id: descriptionText Layout.fillWidth: true - Layout.topMargin: root.description !== "" ? Tokens.spacing.smaller : 0 + Layout.topMargin: root.description !== "" ? Tokens.spacing.medium : 0 Layout.bottomMargin: root.description !== "" ? Tokens.spacing.small : 0 visible: root.description !== "" text: root.description color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.Wrap } } diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index d7885c30..b75272f6 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -73,23 +73,23 @@ RowLayout { root.isEditing = false; } - padding: Tokens.padding.small - leftPadding: Tokens.padding.normal - rightPadding: Tokens.padding.normal + padding: Tokens.padding.extraSmall + leftPadding: Tokens.padding.medium + rightPadding: Tokens.padding.medium background: StyledRect { implicitWidth: 100 - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainerHigh } } StyledRect { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small StateLayer { id: upState @@ -120,11 +120,11 @@ RowLayout { } StyledRect { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: downIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: downIcon.implicitHeight + Tokens.padding.small StateLayer { id: downState diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 1d8c85a9..5dd8b641 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -67,47 +67,29 @@ Slider { property bool moving - function update(): void { - animate = !moving; - binding.when = moving; - font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger; - font.family = moving ? Tokens.font.family.sans : Tokens.font.family.material; - } - - text: root.icon - color: Colours.palette.m3inverseOnSurface anchors.centerIn: parent + anchors.verticalCenterOffset: 1 + text: moving ? Math.round(root.value * 100) : root.icon + color: Colours.palette.m3inverseOnSurface + font: moving ? Tokens.font.body.small : Tokens.font.icon.medium - onMovingChanged: anim.restart() - - Binding { - id: binding - - target: icon - property: "text" - value: Math.round(root.value * 100) - when: false - } - - SequentialAnimation { - id: anim - - Anim { - target: icon - property: "scale" - to: 0 - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardAccel - } - ScriptAction { - script: icon.update() - } - Anim { - target: icon - property: "scale" - to: 1 - duration: Tokens.anim.durations.normal / 2 - easing: Tokens.anim.standardDecel + Behavior on moving { + SequentialAnimation { + Anim { + target: icon + property: "scale" + to: 0.3 + duration: Tokens.anim.durations.small / 2 + easing: Tokens.anim.standardAccel + } + PropertyAction {} + Anim { + target: icon + property: "scale" + to: 1 + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardDecel + } } } } diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index 58af806a..60316083 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -3,80 +3,50 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ButtonBase { id: root - enum Type { - Filled, - Tonal, - Text - } - property alias icon: label.text - property bool checked - property bool toggle - property real padding: type === IconButton.Text ? Tokens.padding.small / 2 : Tokens.padding.smaller - property alias font: label.font - property int type: IconButton.Filled - property bool disabled + readonly property alias label: label - property alias stateLayer: stateLayer - property alias label: label - property alias radiusAnim: radiusAnim + font: Tokens.font.icon.medium + padding: type === IconButton.Text ? Tokens.padding.extraSmall / 2 : Tokens.padding.small - property bool internalChecked - property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary - property color inactiveColour: { - if (!toggle && type === IconButton.Filled) + activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + inactiveColour: { + if (!isToggle && type === IconButton.Filled) return Colours.palette.m3primary; return type === IconButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; } - property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary - property color inactiveOnColour: { - if (!toggle && type === IconButton.Filled) + activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary + inactiveOnColour: { + if (!isToggle && type === IconButton.Filled) return Colours.palette.m3onPrimary; return type === IconButton.Tonal ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant; } - property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) - property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - - signal clicked - - onCheckedChanged: internalChecked = checked - - radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour implicitWidth: implicitHeight - implicitHeight: label.implicitHeight + padding * 2 - - StateLayer { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - disabled: root.disabled - onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } + implicitHeight: { + // Ensure even size so icon is centered properly + const h = label.implicitHeight + padding * 2; + if (h % 2 !== 0) + return h + 1; + return h; } MaterialIcon { id: label anchors.centerIn: parent - color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour - fill: !root.toggle || root.internalChecked ? 1 : 0 + anchors.verticalCenterOffset: 1 // AHHHHHHH material symbols whyyyy + color: root.onColour + fontStyle: root.font + fill: !root.isToggle || root.internalChecked ? 1 : 0 Behavior on fill { - Anim {} - } - } - - Behavior on radius { - Anim { - id: radiusAnim + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index 2a1dacec..8d1bf264 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -4,55 +4,23 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ButtonBase { id: root - enum Type { - Filled, - Tonal, - Text - } - property alias icon: iconLabel.text property alias text: label.text - property bool checked - property bool toggle - property real horizontalPadding: Tokens.padding.normal - property real verticalPadding: Tokens.padding.smaller - property alias font: label.font - property int type: IconTextButton.Filled - property alias stateLayer: stateLayer - property alias iconLabel: iconLabel - property alias label: label + readonly property alias iconLabel: iconLabel + readonly property alias label: label - property bool internalChecked - property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary - property color inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer - property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary - property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - - signal clicked - - onCheckedChanged: internalChecked = checked - - radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer + activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary + inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer implicitWidth: row.implicitWidth + horizontalPadding * 2 implicitHeight: row.implicitHeight + verticalPadding * 2 - StateLayer { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } - } - RowLayout { id: row @@ -64,11 +32,13 @@ StyledRect { Layout.alignment: Qt.AlignVCenter Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + color: root.onColour fill: root.internalChecked ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -77,11 +47,7 @@ StyledRect { Layout.alignment: Qt.AlignVCenter Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575) - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + color: root.onColour } } - - Behavior on radius { - Anim {} - } } diff --git a/components/controls/LoadingIndicator.qml b/components/controls/LoadingIndicator.qml new file mode 100644 index 00000000..8503c889 --- /dev/null +++ b/components/controls/LoadingIndicator.qml @@ -0,0 +1,116 @@ +import QtQuick +import Quickshell +import M3Shapes +import qs.components +import qs.services + +MaterialShape { + id: root + + property list shapes: { + if (containsIcon) + return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval]; + return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval]; + } + property int shapeIndex + property real cRotation + property real lRotation + property real thisLRotation + property bool containsIcon + + property bool animated: true + property int morphAnimRotation: 60 + property real morphScale: 0.14 + property alias rotateAnimDuration: rotateAnim.duration + + property real stiffness: 180 + property real dampingRatio: 0.6 + property real visibilityThreshold: 0.075 + + readonly property real springDuration: { + const wn = Math.sqrt(stiffness); + const r = -dampingRatio * wn; + const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio); + return Math.log(visibilityThreshold / c) / r; + } + readonly property real springMaxVelocity: { + const wn = Math.sqrt(stiffness); + const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z)); + return wn * factor; + } + property bool springSettled: true + + function spring(t: real): var { + const wn = Math.sqrt(stiffness); + const za = dampingRatio * wn; + + const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio); + const r = za / wd; + const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t)); + const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t); + + return [pos, vel]; + } + + implicitSize: 38 + color: Colours.palette.m3primary + toShape: shapes[0] + + ElapsedTimer { + id: timer + } + + FrameAnimation { + running: root.animated && !root.springSettled + onTriggered: { + const t = timer.elapsed(); + + if (t >= root.springDuration) { + root.springSettled = true; + } else { + const [pos, vel] = root.spring(t); + root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird + root.thisLRotation = pos * root.morphAnimRotation; + root.scale = 1 + vel * root.morphScale / root.springMaxVelocity; + } + } + } + + Timer { + interval: 650 + repeat: true + triggeredOnStart: true + running: root.animated + onTriggered: { + root.beginBatchUpdate(); + root.fromShape = root.toShape; + root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length; + root.toShape = root.shapes[root.shapeIndex]; + root.morphProgress = 0; + + root.rotation = root.rotation; + root.lRotation = (root.lRotation + root.thisLRotation) % 360; + root.thisLRotation = 0; + root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation); + + root.springSettled = false; + timer.restart(); + root.endBatchUpdate(); + } + } + + RotationAnimation on cRotation { + id: rotateAnim + + running: root.animated + from: 0 + to: 360 + easing.type: Easing.Linear + loops: Animation.Infinite + duration: 4666 + } + + Behavior on color { + CAnim {} + } +} diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 482a4d40..ffa5d14e 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -48,7 +48,7 @@ MouseArea { Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.DefaultEffects } } @@ -79,7 +79,7 @@ MouseArea { return item.mapToItem(root.parent, 0, off).y + root.marginY; } - radius: Tokens.rounding.normal + radius: Tokens.rounding.large level: 2 implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2) @@ -90,9 +90,7 @@ MouseArea { origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0 Behavior on yScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } @@ -105,7 +103,7 @@ MouseArea { id: column anchors.fill: parent - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall spacing: 0 Repeater { @@ -121,14 +119,14 @@ MouseArea { readonly property bool active: modelData === root.active Layout.fillWidth: true - implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.medium * 2 - radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed - topLeftRadius: index === 0 ? Tokens.rounding.small : radius - topRightRadius: index === 0 ? Tokens.rounding.small : radius - bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius - bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius + radius: active ? Tokens.rounding.medium : Tokens.rounding.extraSmall + topLeftRadius: index === 0 ? Tokens.rounding.medium : radius + topRightRadius: index === 0 ? Tokens.rounding.medium : radius + bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.medium : radius + bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.medium : radius color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0) @@ -156,7 +154,7 @@ MouseArea { id: menuOptionRow anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium spacing: Tokens.spacing.small MaterialIcon { diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index dc52c19c..edce6639 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -15,8 +15,8 @@ StyledRect { property var onValueModified: function (value) {} Layout.fillWidth: true - implicitHeight: row.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -30,7 +30,7 @@ StyledRect { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index 4acd98cf..2feaf615 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -12,43 +12,46 @@ Row { Tonal } - property real horizontalPadding: Tokens.padding.normal - property real verticalPadding: Tokens.padding.smaller + property real horizontalPadding: Tokens.padding.medium + property real verticalPadding: Tokens.padding.small property int type: SplitButton.Filled property bool disabled property bool menuOnTop property string fallbackIcon property string fallbackText + property real minLeftWidth property alias menuItems: menu.items property alias active: menu.active property alias expanded: menu.expanded - property alias menu: menu - property alias iconLabel: iconLabel - property alias label: label - property alias stateLayer: stateLayer + readonly property alias menu: menu + readonly property alias iconLabel: iconLabel + readonly property alias label: label + readonly property alias stateLayer: stateLayer + readonly property alias textRow: textRow + readonly property alias expandBtn: expandBtn property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - spacing: Math.floor(Tokens.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.extraSmall) StyledRect { radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - topRightRadius: Tokens.rounding.small / 2 - bottomRightRadius: Tokens.rounding.small / 2 + topRightRadius: Tokens.rounding.medium / 2 + bottomRightRadius: Tokens.rounding.medium / 2 color: root.disabled ? root.disabledColour : root.colour - implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 + implicitWidth: Math.max(root.minLeftWidth, textRow.implicitWidth + root.horizontalPadding * 2) implicitHeight: expandBtn.implicitHeight StateLayer { id: stateLayer - rect.topRightRadius: parent.topRightRadius - rect.bottomRightRadius: parent.bottomRightRadius + topRightRadius: parent.topRightRadius + bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled onClicked: root.active?.clicked() @@ -93,7 +96,7 @@ Row { StyledRect { id: expandBtn - property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.small / 2 + property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.medium / 2 radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) topLeftRadius: rad diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index bd1ecc62..a907ed2b 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -20,8 +20,8 @@ StyledRect { signal selected(item: MenuItem) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: false @@ -33,7 +33,7 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index ff7772ca..5bed09a4 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -21,14 +21,14 @@ Item { signal editingFinished implicitWidth: 70 - implicitHeight: inputField.implicitHeight + Tokens.padding.small * 2 + implicitHeight: inputField.implicitHeight + Tokens.padding.small StyledRect { id: container anchors.fill: parent color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) opacity: root.enabled ? 1 : 0.5 @@ -54,7 +54,7 @@ Item { id: inputField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: root.horizontalAlignment validator: root.validator readOnly: root.readOnly diff --git a/components/controls/StyledProgressBar.qml b/components/controls/StyledProgressBar.qml new file mode 100644 index 00000000..399b66fd --- /dev/null +++ b/components/controls/StyledProgressBar.qml @@ -0,0 +1,247 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Templates +import Caelestia +import Caelestia.Components +import Caelestia.Config +import Caelestia.Internal +import qs.components +import qs.services + +ProgressBar { + id: root + + enum IndeterminateAnimState { + Running, + Completing, + Stopped + } + + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + property bool wavy + property bool wavePaused + property int waveFrequency: 6 + property real waveAmplitude: 0.5 + property int waveDuration: 1000 + + property int indeterminateAnimState: StyledProgressBar.Stopped + + function toBounds(startFrac: real, endFrac: real, gapSize: real): point { + startFrac = CUtils.clamp(startFrac, 0, 1); + endFrac = CUtils.clamp(endFrac, 0, 1); + + // Ramp down gap size + const GAP_RAMP_DOWN_THRESHOLD = 0.01; + gapSize += height / 2; + const startGapSize = (gapSize * CUtils.clamp(startFrac, 0, GAP_RAMP_DOWN_THRESHOLD) / GAP_RAMP_DOWN_THRESHOLD); + const endGapSize = (gapSize * (1 - CUtils.clamp(endFrac, 1 - GAP_RAMP_DOWN_THRESHOLD, 1)) / GAP_RAMP_DOWN_THRESHOLD); + const start = width * startFrac + startGapSize; + const end = width * endFrac - endGapSize; + + return start >= end ? Qt.point(0, 0) : Qt.point(start, end); + } + + function updateIAnimState(): void { + if (indeterminate) { + manager.completeEndProgress = 0; + indeterminateAnimState = StyledProgressBar.Running; + } else if (indeterminateAnimState === StyledProgressBar.Running) { + indeterminateAnimState = StyledProgressBar.Completing; + } + } + + onIndeterminateChanged: updateIAnimState() + Component.onCompleted: updateIAnimState() + + implicitWidth: 200 + implicitHeight: 4 + + contentItem: Loader { + anchors.fill: parent + asynchronous: true + sourceComponent: root.indeterminate || root.indeterminateAnimState !== StyledProgressBar.Stopped ? indeterminateComp : determinateComp + } + + LinearIndicatorManager { + id: manager + + gap: root.Tokens.spacing.extraSmall + + Anim on progress { + running: root.indeterminateAnimState !== StyledProgressBar.Stopped + from: 0 + to: 1 + duration: manager.duration + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Anim on completeEndProgress { + running: root.indeterminateAnimState === StyledProgressBar.Completing + to: 1 + duration: manager.completeEndDuration + onFinished: { + if (root.indeterminateAnimState === StyledProgressBar.Completing) + root.indeterminateAnimState = StyledProgressBar.Stopped; + } + } + } + + Component { + id: determinateComp + + Item { + Line { + id: remaining + + anchors.right: parent.right + implicitWidth: parent.width - wave.implicitWidth - Tokens.spacing.extraSmall + } + + Line { + property real implicitSize + + Component.onCompleted: implicitSize = Qt.binding(() => parent.width - wave.implicitWidth < parent.height ? parent.height : 4) + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: (parent.height - implicitHeight) / 2 + + implicitWidth: implicitSize + implicitHeight: implicitSize + + radius: Tokens.rounding.full + color: root.fgColour + + Behavior on implicitSize { + Anim { + type: Anim.FastSpatial + } + } + } + + Wave { + id: wave + + anchors.left: parent.left + Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition) + + Behavior on implicitWidth { + Anim {} + } + } + } + } + + Component { + id: indeterminateComp + + Item { + id: content + + Line { + bounds: { + const i = manager.activeIndicators[0]; // qmllint disable unresolved-type + return i ? root.toBounds(0, i.startFraction, i.gapSize / 2) : Qt.point(0, 0); + } + } + + Line { + bounds: { + const i = manager.activeIndicators[manager.activeIndicators.length - 1]; // qmllint disable unresolved-type + return i ? root.toBounds(i.endFraction, 1, i.gapSize / 2) : Qt.point(0, 0); + } + } + + Instantiator { + model: Math.max(manager.activeIndicators.length, 1) - 1 // qmllint disable unresolved-type + + delegate: Line { + required property int index + readonly property LinearIndicatorSegment cur: manager.activeIndicators[index] // qmllint disable unresolved-type + readonly property LinearIndicatorSegment next: manager.activeIndicators[index + 1 % manager.activeIndicators.length] // qmllint disable unresolved-type + + bounds: root.toBounds(cur.endFraction, next.startFraction, cur.gapSize / 2) + } + + onObjectAdded: (_, obj) => content.data.push(obj) + onObjectRemoved: (_, obj) => { + const idx = content.data.indexOf(obj); + if (idx !== -1) + content.data.splice(idx, 1); + } + } + + Instantiator { + model: manager.activeIndicators // qmllint disable unresolved-type + + delegate: Wave { + required property LinearIndicatorSegment modelData + readonly property point bounds: root.toBounds(modelData.startFraction, modelData.endFraction, modelData.gapSize / 2) + + x: bounds.x + implicitWidth: bounds.y - bounds.x + + color: root.fgColour + } + + onObjectAdded: (_, obj) => content.data.push(obj) + onObjectRemoved: (_, obj) => { + const idx = content.data.indexOf(obj); + if (idx !== -1) + content.data.splice(idx, 1); + } + } + } + } + + component Line: StyledRect { + property point bounds + + x: bounds.x + implicitWidth: bounds.y - bounds.x + + anchors.verticalCenter: parent.verticalCenter + implicitHeight: parent.height + + radius: Tokens.rounding.full + color: root.bgColour + } + + component Wave: WavyLine { + id: wave + + anchors.verticalCenter: parent.verticalCenter + implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth + + lineWidth: parent.height + amplitudeMultiplier: root.wavy ? root.waveAmplitude : 0 + frequency: root.waveFrequency + startX: x + fullLength: parent.width + color: root.fgColour + + Anim on waveProgress { + running: true + paused: wave.amplitudeMultiplier === 0 || root.wavePaused + from: 0 + to: 1 + duration: root.waveDuration + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Behavior on amplitudeMultiplier { + Anim { + type: Anim.DefaultEffects + } + } + + Behavior on color { + CAnim {} + } + } +} diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index 24c7d6e8..a12711b1 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -7,7 +7,7 @@ import qs.services RadioButton { id: root - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) @@ -24,7 +24,7 @@ RadioButton { anchors.verticalCenter: parent.verticalCenter StateLayer { - anchors.margins: -Tokens.padding.smaller + anchors.margins: -Tokens.padding.small color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 onClicked: root.click() @@ -46,9 +46,9 @@ RadioButton { contentItem: StyledText { text: root.text - font.pointSize: root.font.pointSize + font: root.font anchors.verticalCenter: parent.verticalCenter anchors.left: outerCircle.right - anchors.leftMargin: Tokens.spacing.smaller + anchors.leftMargin: Tokens.spacing.medium } } diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index 8cfe3f8d..6a61391c 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -45,7 +45,7 @@ ScrollBar { } } } - implicitWidth: Tokens.padding.small + implicitWidth: Tokens.padding.extraSmall contentItem: StyledRect { anchors.left: parent.left @@ -74,7 +74,9 @@ ScrollBar { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index f169691b..91c8040a 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -1,5 +1,9 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Templates +import Caelestia +import Caelestia.Components import Caelestia.Config import qs.components import qs.services @@ -7,51 +11,177 @@ import qs.services Slider { id: root - background: Item { + property bool wavy + property bool animateWave + property real waveFrequency: 6 + property int waveDuration: 1000 + + property color fgColour: enabled ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3onSurface, 0.38) + property color bgColour: enabled ? Colours.palette.m3secondaryContainer : Qt.alpha(Colours.palette.m3onSurface, 0.1) + + property real pos: visualPosition + property real filledWidth + + signal interaction(v: real) + + Component.onCompleted: filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos) + + implicitWidth: 200 + implicitHeight: 12 + + contentItem: Item { + anchors.fill: parent + StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.topMargin: root.implicitHeight / 3 - anchors.bottomMargin: root.implicitHeight / 3 + id: remaining - implicitWidth: root.handle.x - root.implicitHeight / 6 + anchors.left: handle.right + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Tokens.spacing.extraSmall - color: Colours.palette.m3primary - radius: Tokens.rounding.full - topRightRadius: root.implicitHeight / 15 - bottomRightRadius: root.implicitHeight / 15 + implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1)) + opacity: Math.min(width, 12) / 12 + + radius: Tokens.rounding.medium + topLeftRadius: Tokens.rounding.extraSmall / 2 + bottomLeftRadius: Tokens.rounding.extraSmall / 2 + color: root.bgColour } StyledRect { - anchors.top: parent.top - anchors.bottom: parent.bottom anchors.right: parent.right - anchors.topMargin: root.implicitHeight / 3 - anchors.bottomMargin: root.implicitHeight / 3 + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 4 * remaining.opacity - implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 + implicitWidth: implicitHeight + implicitHeight: 4 * remaining.opacity + opacity: remaining.opacity - color: Colours.palette.m3surfaceContainerHighest radius: Tokens.rounding.full - topLeftRadius: root.implicitHeight / 15 - bottomLeftRadius: root.implicitHeight / 15 + color: root.fgColour + } + + StyledRect { + id: handle + + anchors.left: filled.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Tokens.spacing.extraSmall + + implicitWidth: 4 + implicitHeight: { + const mult = parent.height <= 12 ? 3 : 1.2; + const pressMult = parent.height <= 12 ? 4 : 1.5; + return parent.height * (mouse.pressed ? pressMult : mult); + } + + radius: Tokens.rounding.full + color: root.fgColour + + 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 + + StyledRect { + implicitWidth: root.filledWidth + implicitHeight: root.height + + radius: Tokens.rounding.medium + topRightRadius: Tokens.rounding.extraSmall / 2 + bottomRightRadius: Tokens.rounding.extraSmall / 2 + color: root.fgColour + } + } + + Component { + id: waveComp + + WavyLine { + lineWidth: root.height * 0.7 + frequency: root.waveFrequency + startX: x + fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin + color: root.fgColour + + implicitWidth: root.filledWidth + implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth + + Anim on waveProgress { + running: true + paused: !root.animateWave + from: 0 + to: 1 + duration: root.waveDuration + easing.type: Easing.Linear + loops: Animation.Infinite + } + + Behavior on color { + CAnim {} + } + } } } - handle: StyledRect { - x: root.visualPosition * root.availableWidth - implicitWidth / 2 + Binding { + id: posBinding - implicitWidth: root.implicitHeight / 4.5 - implicitHeight: root.implicitHeight + target: root + property: "pos" + value: CUtils.clamp(mouse.pressStartPos + mouse.dragMovement, 0, 1) + when: mouse.pressed + } - color: Colours.palette.m3primary - radius: Tokens.rounding.full + MouseArea { + id: mouse - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: Qt.PointingHandCursor + property real pressStartX + property real pressStartPos + property real dragMovement + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + preventStealing: true + implicitHeight: handle.implicitHeight + + onPressed: e => { + widthBehavior.enabled = false; + pressStartX = e.x; + pressStartPos = root.visualPosition; } + onPositionChanged: e => { + dragMovement = (e.x - pressStartX) / width; + root.interaction(posBinding.value); + } + onReleased: e => { + root.interaction(posBinding.value); + widthBehavior.enabled = true; + dragMovement = 0; + } + } + + Behavior on filledWidth { + id: widthBehavior + + Anim {} } } diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index 2e31adbf..4af5e51e 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -18,17 +18,17 @@ Switch { color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) implicitWidth: implicitHeight * 1.7 - implicitHeight: Tokens.font.size.normal + Tokens.padding.smaller * 2 + implicitHeight: Tokens.font.body.medium.pointSize + Tokens.padding.small * 2 StyledRect { - readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.2 : implicitHeight radius: Tokens.rounding.full color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) - x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.small / 2 : Tokens.padding.small / 2 + x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.extraSmall / 2 : Tokens.padding.extraSmall / 2 implicitWidth: nonAnimWidth - implicitHeight: parent.implicitHeight - Tokens.padding.small + implicitHeight: parent.implicitHeight - Tokens.padding.extraSmall anchors.verticalCenter: parent.verticalCenter StyledRect { @@ -39,7 +39,9 @@ Switch { opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -83,12 +85,12 @@ Switch { anchors.centerIn: parent width: height - height: parent.implicitHeight - Tokens.padding.small * 2 + height: parent.implicitHeight - Tokens.padding.medium preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { - strokeWidth: root.Tokens.font.size.larger * 0.15 + strokeWidth: root.Tokens.font.body.large.pointSize * 0.15 strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest fillColor: "transparent" capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap @@ -129,11 +131,15 @@ Switch { } Behavior on x { - Anim {} + Anim { + type: Anim.FastSpatial + } } Behavior on implicitWidth { - Anim {} + Anim { + type: Anim.FastSpatial + } } } } @@ -145,7 +151,7 @@ Switch { } component PropAnim: PropertyAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + duration: Tokens.anim.durations.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 4ab2f824..85cf4fa6 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -11,8 +11,7 @@ TextField { color: Colours.palette.m3onSurface placeholderTextColor: Colours.palette.m3outline - font.family: Tokens.font.family.sans - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering cursorVisible: !readOnly @@ -25,7 +24,7 @@ TextField { implicitWidth: 2 color: Colours.palette.m3primary - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Connections { function onCursorPositionChanged(): void { diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index b909739f..d4d83183 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -13,8 +13,8 @@ StyledRect { signal toggled(checked: bool) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -28,7 +28,7 @@ StyledRect { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index 94d76ff8..5cfff6e8 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -3,75 +3,41 @@ import Caelestia.Config import qs.components import qs.services -StyledRect { +ButtonBase { id: root - enum Type { - Filled, - Tonal, - Text - } - property alias text: label.text - property bool checked - property bool toggle - property real horizontalPadding: Tokens.padding.normal - property real verticalPadding: Tokens.padding.smaller - property alias font: label.font - property int type: TextButton.Filled + readonly property alias label: label - property alias stateLayer: stateLayer - property alias label: label + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small - property bool internalChecked - property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary - property color inactiveColour: { - if (!toggle && type === TextButton.Filled) + activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + inactiveColour: { + if (!isToggle && type === TextButton.Filled) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; } - property color activeOnColour: { + activeOnColour: { if (type === TextButton.Text) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary; } - property color inactiveOnColour: { - if (!toggle && type === TextButton.Filled) + inactiveOnColour: { + if (!isToggle && type === TextButton.Filled) return Colours.palette.m3onPrimary; if (type === TextButton.Text) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer; } - signal clicked - - onCheckedChanged: internalChecked = checked - - radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour - implicitWidth: label.implicitWidth + horizontalPadding * 2 implicitHeight: label.implicitHeight + verticalPadding * 2 - StateLayer { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } - } - StyledText { id: label anchors.centerIn: parent - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - } - - Behavior on radius { - Anim {} + color: root.onColour } } diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 232426c1..cd6427a2 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -14,9 +14,9 @@ StyledRect { property string icon property string label property string accent: "Secondary" - property real iconSize: Tokens.font.size.large + property real iconSize: Tokens.font.icon.large.pointSize property real horizontalPadding: Tokens.padding.large - property real verticalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.medium property string tooltip: "" property bool hovered: false @@ -24,10 +24,10 @@ StyledRect { Component.onCompleted: hovered = toggleStateLayer.containsMouse - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.medium * 2 : toggled ? Tokens.padding.small : 0) implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 - radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.small : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) + radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.medium : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] Connections { @@ -52,7 +52,7 @@ StyledRect { id: toggleBtnInner anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { id: toggleBtnIcon @@ -61,10 +61,12 @@ StyledRect { fill: root.toggled ? 1 : 0 text: root.icon color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] - font.pointSize: root.iconSize + fontStyle: Tokens.font.icon.size(root.iconSize).build() Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index beb6a15c..133c3ff4 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -12,7 +12,7 @@ RowLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index 6c62f43d..162bd691 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -47,7 +47,7 @@ Popup { let newY = targetPos.y - tooltipHeight - Tokens.spacing.small; // Keep within bounds - const padding = Tokens.padding.normal; + const padding = Tokens.padding.medium; if (newX < padding) { newX = padding; } else if (newX + tooltipWidth > (parent.width - padding)) { @@ -116,11 +116,11 @@ Popup { contentItem: StyledRect { id: tooltipRect - implicitWidth: tooltipText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: tooltipText.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: tooltipText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: tooltipText.implicitHeight + Tokens.padding.large color: Colours.palette.m3surfaceContainerHighest - radius: Tokens.rounding.small + radius: Tokens.rounding.medium antialiasing: true // Add elevation for depth @@ -138,7 +138,7 @@ Popup { text: root.text color: Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml index 9e0962da..9b8822c5 100644 --- a/components/effects/Elevation.qml +++ b/components/effects/Elevation.qml @@ -13,6 +13,8 @@ RectangularShadow { offset.y: dp / 2 Behavior on dp { - Anim {} + Anim { + type: Anim.SlowEffects + } } } diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index 1b502e29..c16800f0 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -37,8 +37,8 @@ StyledRect { id: maskInner anchors.fill: parent - anchors.margins: Tokens.padding.normal - radius: Tokens.rounding.normal + anchors.margins: Tokens.padding.medium + radius: Tokens.rounding.large } } } diff --git a/components/effects/Mask.qml b/components/effects/Mask.qml new file mode 100644 index 00000000..fb463adf --- /dev/null +++ b/components/effects/Mask.qml @@ -0,0 +1,7 @@ +import QtQuick.Effects + +MultiEffect { + maskEnabled: true + maskSpreadAtMin: 1 + maskThresholdMin: 0.5 +} diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml deleted file mode 100644 index 8e625034..00000000 --- a/components/effects/OpacityMask.qml +++ /dev/null @@ -1,9 +0,0 @@ -import QtQuick -import Quickshell - -ShaderEffect { - required property Item source - required property Item maskSource - - fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") -} diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index 83cb4a92..4151c8e0 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -9,8 +9,8 @@ Item { required property var currentItem - implicitWidth: content.implicitWidth + Tokens.padding.larger + content.anchors.rightMargin - implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.normal + content.anchors.bottomMargin : 0 + implicitWidth: content.implicitWidth + Tokens.padding.medium + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.medium + content.anchors.bottomMargin : 0 Shape { preferredRendererType: Shape.CurveRenderer @@ -18,7 +18,7 @@ Item { ShapePath { id: path - readonly property real rounding: Tokens.rounding.small + readonly property real rounding: root.Tokens.rounding.medium readonly property bool flatten: root.implicitHeight < rounding * 2 readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding @@ -76,8 +76,8 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.rightMargin: Tokens.padding.larger - Tokens.padding.small - anchors.bottomMargin: Tokens.padding.normal - Tokens.padding.small + anchors.rightMargin: Tokens.padding.medium - Tokens.padding.extraSmall + anchors.bottomMargin: Tokens.padding.medium - Tokens.padding.extraSmall Connections { function onCurrentItemChanged(): void { diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index b46b4f72..fe686e4a 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -9,7 +9,7 @@ StyledRect { required property var dialog required property FolderContents folder - implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.medium * 2 color: Colours.tPalette.m3surfaceContainer @@ -17,7 +17,7 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium spacing: Tokens.spacing.small @@ -28,14 +28,14 @@ StyledRect { StyledRect { Layout.fillWidth: true Layout.fillHeight: true - Layout.rightMargin: Tokens.spacing.normal + Layout.rightMargin: Tokens.spacing.medium color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.small + radius: Tokens.rounding.medium StyledText { anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` } @@ -43,10 +43,10 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.medium * 2 StateLayer { disabled: !root.dialog.selectionValid @@ -57,7 +57,7 @@ StyledRect { id: selectText anchors.centerIn: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium text: qsTr("Select") color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -66,10 +66,10 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -81,7 +81,7 @@ StyledRect { id: cancelText anchors.centerIn: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium text: qsTr("Cancel") } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index d8869b83..6238535d 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects import QtQuick.Layouts import Quickshell import Caelestia.Config import Caelestia.Models import qs.components import qs.components.controls +import qs.components.effects import qs.components.filedialog import qs.components.images import qs.services @@ -24,12 +24,9 @@ Item { color: Colours.tPalette.m3surfaceContainer layer.enabled: true - layer.effect: MultiEffect { + layer.effect: Mask { maskSource: mask - maskEnabled: true maskInverted: true - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 } } @@ -42,8 +39,8 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Tokens.padding.small - radius: Tokens.rounding.small + anchors.margins: Tokens.padding.extraSmall + radius: Tokens.rounding.medium } } @@ -59,20 +56,20 @@ Item { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 500 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.Medium).build() } StyledText { text: qsTr("This folder is empty") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -80,10 +77,10 @@ Item { id: view anchors.fill: parent - anchors.margins: Tokens.padding.small + Tokens.padding.normal + anchors.margins: Tokens.padding.extraSmall + Tokens.padding.medium cellWidth: Sizes.itemWidth + Tokens.spacing.small - cellHeight: Sizes.itemWidth + Tokens.spacing.small * 2 + Tokens.padding.normal * 2 + 1 + cellHeight: Sizes.itemWidth + Tokens.spacing.large + Tokens.padding.medium * 2 + 1 clip: true focus: true @@ -120,12 +117,12 @@ Item { properties: "opacity,scale" from: 0 to: 1 - type: Anim.DefaultSpatial } } remove: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" to: 0 } @@ -137,13 +134,13 @@ Item { displaced: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,scale" to: 1 easing: Tokens.anim.standardDecel } Anim { properties: "x,y" - type: Anim.DefaultSpatial } } } @@ -151,7 +148,7 @@ Item { CurrentItem { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall currentItem: view.currentItem } @@ -162,12 +159,12 @@ Item { required property int index required property FileSystemEntry modelData - readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.normal * 2 + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.medium * 2 implicitWidth: Sizes.itemWidth implicitHeight: nonAnimHeight - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 clip: true @@ -187,9 +184,9 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: Tokens.padding.normal + anchors.topMargin: Tokens.padding.medium - implicitSize: Sizes.itemWidth - Tokens.padding.normal * 2 + implicitSize: Sizes.itemWidth - Tokens.padding.medium * 2 Component.onCompleted: { const file = item.modelData; @@ -211,7 +208,7 @@ Item { anchors.right: parent.right anchors.top: icon.bottom anchors.topMargin: Tokens.spacing.small - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium horizontalAlignment: Text.AlignHCenter elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index c53d8f76..19cb75a5 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -11,8 +11,8 @@ StyledRect { required property var dialog - implicitWidth: inner.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: inner.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.medium * 2 color: Colours.tPalette.m3surfaceContainer @@ -20,15 +20,15 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium spacing: Tokens.spacing.small Item { implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small StateLayer { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium disabled: root.dialog.cwd.length === 1 onClicked: root.dialog.cwd.pop() } @@ -46,7 +46,7 @@ StyledRect { StyledRect { Layout.fillWidth: true - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainerHigh implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 @@ -55,7 +55,7 @@ StyledRect { id: pathComponents anchors.fill: parent - anchors.margins: Tokens.padding.small / 2 + anchors.margins: Tokens.padding.extraSmall / 2 anchors.leftMargin: 0 spacing: Tokens.spacing.small @@ -78,13 +78,13 @@ StyledRect { sourceComponent: StyledText { text: "/" color: Colours.palette.m3onSurfaceVariant - font.bold: true + font: Tokens.font.body.builders.small.weight(Font.Bold).build() } } Item { - implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.small : 0) + folderName.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: folderName.implicitHeight + Tokens.padding.small * 2 + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.extraSmall : 0) + folderName.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: folderName.implicitHeight + Tokens.padding.small Loader { asynchronous: true @@ -95,7 +95,7 @@ StyledRect { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } - radius: Tokens.rounding.small + radius: Tokens.rounding.medium } } @@ -106,7 +106,7 @@ StyledRect { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.medium active: folder.index === 0 && folder.modelData === "Home" sourceComponent: MaterialIcon { @@ -121,11 +121,11 @@ StyledRect { anchors.left: homeIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: homeIcon.active ? Tokens.padding.small : 0 + anchors.leftMargin: homeIcon.active ? Tokens.padding.extraSmall : 0 text: folder.modelData color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface - font.bold: true + font: Tokens.font.body.builders.small.weight(Font.Bold).build() } } } diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index e9c0918b..611d44e7 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -13,7 +13,7 @@ StyledRect { required property var dialog implicitWidth: Sizes.sidebarWidth - implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.medium * 2 color: Colours.tPalette.m3surfaceContainer @@ -23,17 +23,16 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.small / 2 + anchors.margins: Tokens.padding.medium + spacing: Tokens.spacing.extraSmall StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.padding.small / 2 - Layout.bottomMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.padding.extraSmall / 2 + Layout.bottomMargin: Tokens.spacing.medium text: qsTr("Files") color: Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.larger - font.bold: true + font: Tokens.font.body.builders.large.weight(Font.Bold).build() } Repeater { @@ -46,7 +45,7 @@ StyledRect { readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] Layout.fillWidth: true - implicitHeight: placeInner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: placeInner.implicitHeight + Tokens.padding.medium * 2 radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) @@ -65,11 +64,11 @@ StyledRect { id: placeInner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: Tokens.padding.large anchors.rightMargin: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: { @@ -91,11 +90,13 @@ StyledRect { return "folder"; } color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: place.selected ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -103,7 +104,7 @@ StyledRect { Layout.fillWidth: true text: place.modelData color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } } diff --git a/components/images/FadeImage.qml b/components/images/FadeImage.qml new file mode 100644 index 00000000..ebbb9ab6 --- /dev/null +++ b/components/images/FadeImage.qml @@ -0,0 +1,56 @@ +import QtQuick +import Quickshell +import qs.components + +Image { + id: root + + property bool hadPrevious + + function maybeStartInAnim(): void { + if (!opacityInAnim.running && status === Image.Ready) { + opacityInAnim.type = hadPrevious ? Anim.DefaultEffects : Anim.StandardLarge; + opacityInAnim.start(); + } + } + + asynchronous: true + fillMode: Image.PreserveAspectCrop + + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + retainWhileLoading: true + opacity: 0 + + onStatusChanged: maybeStartInAnim() + + Anim on opacity { + id: opacityInAnim + + running: false + to: 1 + } + + Behavior on source { + SequentialAnimation { + Anim { + target: root + property: "opacity" + to: 0 + type: Anim.FastEffects + } + PropertyAction { + target: root + property: "hadPrevious" + value: root.source + } + PropertyAction {} + ScriptAction { + script: root.maybeStartInAnim() + } + } + } +} diff --git a/components/widgets/CoverArt.qml b/components/widgets/CoverArt.qml new file mode 100644 index 00000000..1c7fbdb0 --- /dev/null +++ b/components/widgets/CoverArt.qml @@ -0,0 +1,107 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.images +import qs.services + +Item { + id: root + + readonly property alias shape: shape + + property bool hadPrevious + property color fallbackColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Slight glow to separate from bg + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 1 + shadowColor: Colours.palette.m3outline + shadowOpacity: 0.3 + } + + Behavior on fallbackColour { + CAnim {} + } + + Item { + id: shapeWrapper + + anchors.fill: parent + layer.enabled: true + opacity: root.fallbackColour.a + + MaterialShape { + id: shape + + implicitSize: root.width + shape: MaterialShape.Cookie12Sided + color: Qt.alpha(root.fallbackColour, 1) + + Anim on rotation { + running: true + paused: !Players.active?.isPlaying + from: 360 + to: 0 + duration: 23500 + easing.type: Easing.Linear + loops: Animation.Infinite + } + } + } + + MaterialIcon { + anchors.centerIn: parent + + grade: 200 + text: image.status === Image.Error ? "broken_image" : "art_track" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build() + opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0 + animate: true + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: image.status === Image.Loading ? 1 : 0 + + sourceComponent: LoadingIndicator { + implicitSize: root.width * 0.3 + color: Colours.palette.m3primaryContainer + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + FadeImage { + id: image + + anchors.fill: parent + + source: Players.getArtUrl(Players.active) + + layer.enabled: true + layer.effect: Mask { + maskSource: shapeWrapper + } + } +} diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index d426eed8..5bd7cb2f 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -8,13 +8,13 @@ StyledRect { required property int extra anchors.right: parent.right - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium color: Colours.palette.m3tertiary - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: count.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: count.implicitHeight + Tokens.padding.small * 2 + implicitWidth: count.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: count.implicitHeight + Tokens.padding.small opacity: extra > 0 ? 1 : 0 scale: extra > 0 ? 1 : 0.5 @@ -38,6 +38,7 @@ StyledRect { Behavior on opacity { Anim { + type: Anim.DefaultEffects duration: Tokens.anim.durations.expressiveFastSpatial } } diff --git a/components/widgets/WavyTopRect.qml b/components/widgets/WavyTopRect.qml new file mode 100644 index 00000000..4d42e7b7 --- /dev/null +++ b/components/widgets/WavyTopRect.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Shapes +import qs.components +import qs.services + +Shape { + id: root + + property color color: Colours.palette.m3surfaceContainer + property int waves: 4 + property real amplitude: 3 + + readonly property real waveHeight: amplitude * 2 + + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + strokeWidth: 0 + strokeColor: "transparent" + fillColor: root.color + + PathSvg { + path: { + const w = root.width; + const h = root.height; + const a = root.amplitude; + const n = Math.max(1, root.waves); + const wl = w / n; + const half = wl / 2; + + let d = `M 0,${a} `; + for (let i = 0; i < n; ++i) { + const x = i * wl; + d += `Q ${x + half / 2},${-a} ${x + half},${a} `; + d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `; + } + d += `L ${w},${h} L 0,${h} Z`; + return d; + } + } + + Behavior on fillColor { + CAnim {} + } + } +} diff --git a/modules/GSFLoader.qml b/modules/GSFLoader.qml new file mode 100644 index 00000000..179d59fc --- /dev/null +++ b/modules/GSFLoader.qml @@ -0,0 +1,6 @@ +import QtQuick +import Quickshell + +FontLoader { + source: Quickshell.shellPath("assets/google-sans-flex/GoogleSansFlex-VariableFont_GRAD,ROND,opsz,slnt,wdth,wght.ttf") +} diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 7a13150c..07777fba 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects import Quickshell import Quickshell.Io import Quickshell.Wayland import Caelestia import qs.components +import qs.components.effects import qs.services MouseArea { @@ -168,17 +168,17 @@ MouseArea { to: 0 type: Anim.StandardLarge } - ExAnim { + Anim { target: root properties: "rsx,rsy" to: 0 } - ExAnim { + Anim { target: root property: "sw" to: root.screen.width } - ExAnim { + Anim { target: root property: "sh" to: root.screen.height @@ -230,12 +230,9 @@ MouseArea { opacity: 0.3 layer.enabled: true - layer.effect: MultiEffect { + layer.effect: Mask { maskSource: selectionWrapper - maskEnabled: true maskInverted: true - maskSpreadAtMin: 1 - maskThresholdMin: 0.5 } } @@ -284,28 +281,24 @@ MouseArea { Behavior on rsx { enabled: !root.pressed - ExAnim {} + Anim {} } Behavior on rsy { enabled: !root.pressed - ExAnim {} + Anim {} } Behavior on sw { enabled: !root.pressed - ExAnim {} + Anim {} } Behavior on sh { enabled: !root.pressed - ExAnim {} - } - - component ExAnim: Anim { - type: Anim.DefaultSpatial + Anim {} } } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 70f6914d..c361123f 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -57,8 +57,8 @@ Variants { asynchronous: true active: Config.background.desktopClock.enabled - anchors.margins: Tokens.padding.large * 2 - anchors.leftMargin: Tokens.padding.large * 2 + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) + anchors.margins: Tokens.padding.extraLargeIncreased + anchors.leftMargin: Tokens.padding.extraLargeIncreased + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.small, Config.border.thickness) state: Config.background.desktopClock.position states: [ diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index c7415be8..6a14d41e 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -24,7 +24,7 @@ Item { readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary implicitWidth: layout.implicitWidth + (Tokens.padding.large * 4 * root.clockScale) - implicitHeight: layout.implicitHeight + (Tokens.padding.large * 2 * root.clockScale) + implicitHeight: layout.implicitHeight + (Tokens.padding.extraLargeIncreased * root.clockScale) Item { id: clockContainer @@ -63,7 +63,7 @@ Item { visible: root.bgEnabled anchors.fill: parent - radius: Tokens.rounding.large * root.clockScale + radius: Tokens.rounding.extraLarge * root.clockScale opacity: Config.background.desktopClock.background.opacity color: Colours.palette.m3surface @@ -74,21 +74,20 @@ Item { id: layout anchors.centerIn: parent - spacing: Tokens.spacing.larger * root.clockScale + spacing: Tokens.spacing.large * root.clockScale RowLayout { spacing: Tokens.spacing.small StyledText { text: Time.hourStr - font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - font.weight: Font.Bold + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * 3 * root.clockScale).weight(Font.Bold).build() color: root.safePrimary } StyledText { text: ":" - font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * 3 * root.clockScale).build() color: root.safeTertiary opacity: 0.8 Layout.topMargin: -Tokens.padding.large * 1.5 * root.clockScale @@ -96,8 +95,7 @@ Item { StyledText { text: Time.minuteStr - font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - font.weight: Font.Bold + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * 3 * root.clockScale).weight(Font.Bold).build() color: root.safeSecondary } @@ -111,7 +109,7 @@ Item { sourceComponent: StyledText { text: Time.amPmStr - font.pointSize: Tokens.font.size.large * root.clockScale + font: Tokens.font.clock.size(Tokens.font.title.medium.pointSize * root.clockScale).build() color: root.safeSecondary } } @@ -120,8 +118,8 @@ Item { StyledRect { Layout.fillHeight: true Layout.preferredWidth: 4 * root.clockScale - Layout.topMargin: Tokens.spacing.larger * root.clockScale - Layout.bottomMargin: Tokens.spacing.larger * root.clockScale + Layout.topMargin: Tokens.spacing.large * root.clockScale + Layout.bottomMargin: Tokens.spacing.large * root.clockScale radius: Tokens.rounding.full color: root.safePrimary opacity: 0.8 @@ -132,24 +130,19 @@ Item { StyledText { text: Time.format("MMMM").toUpperCase() - font.pointSize: Tokens.font.size.large * root.clockScale - font.letterSpacing: 4 - font.weight: Font.Bold + font: Tokens.font.clock.size(Tokens.font.title.medium.pointSize * root.clockScale).letterSpacing(4).weight(Font.Bold).build() color: root.safeSecondary } StyledText { text: Time.format("dd") - font.pointSize: Tokens.font.size.extraLarge * root.clockScale - font.letterSpacing: 2 - font.weight: Font.Medium + font: Tokens.font.clock.size(Tokens.font.headline.medium.pointSize * root.clockScale).letterSpacing(2).weight(Font.Medium).build() color: root.safePrimary } StyledText { text: Time.format("dddd") - font.pointSize: Tokens.font.size.larger * root.clockScale - font.letterSpacing: 2 + font: Tokens.font.clock.size(Tokens.font.body.large.pointSize * root.clockScale).letterSpacing(2).build() color: root.safeSecondary } } @@ -157,9 +150,7 @@ Item { } Behavior on clockScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on implicitWidth { diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 45055879..fc163379 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -65,8 +65,8 @@ Item { values: Audio.cava.values primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) - rounding: Tokens.rounding.small * Config.background.visualiser.rounding - spacing: Tokens.spacing.small * Config.background.visualiser.spacing + rounding: Tokens.rounding.medium * Config.background.visualiser.rounding + spacing: Tokens.spacing.extraSmall * Config.background.visualiser.spacing animationDuration: Tokens.anim.durations.normal Behavior on anchors.leftMargin { @@ -87,6 +87,8 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 8cc2df9d..21ff34eb 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -12,22 +12,24 @@ Item { id: root property string source: Wallpapers.current - property Image current: one + property CachingImage current property bool completed onSourceChanged: { if (!source) current = null; - else if (current === one) - two.update(); else - one.update(); + current = imgComp.createObject(this, { + path: source + }); } Component.onCompleted: { if (source) Qt.callLater(() => { - one.update(); + current = imgComp.createObject(this, { + path: source + }); completed = true; }); } @@ -43,12 +45,12 @@ Item { Row { anchors.centerIn: parent - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased MaterialIcon { text: "sentiment_stressed" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.extraLarge * 5 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(5).build() } Column { @@ -58,13 +60,12 @@ Item { StyledText { text: qsTr("Wallpaper missing?") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.extraLarge * 2 - font.bold: true + font: Tokens.font.body.builders.large.size(28 * 2).weight(Font.Bold).build() } StyledRect { - implicitWidth: selectWallText.implicitWidth + Tokens.padding.large * 2 - implicitHeight: selectWallText.implicitHeight + Tokens.padding.small * 2 + implicitWidth: selectWallText.implicitWidth + Tokens.padding.extraLargeIncreased + implicitHeight: selectWallText.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full color: Colours.palette.m3primary @@ -91,7 +92,7 @@ Item { text: qsTr("Set it now!") color: Colours.palette.m3onPrimary - font.pointSize: Tokens.font.size.large + font: Tokens.font.body.large } } } @@ -99,48 +100,34 @@ Item { } } - Img { - id: one - } + Component { + id: imgComp - Img { - id: two - } + CachingImage { + id: img - component Img: CachingImage { - id: img + anchors.fill: parent - function update(): void { - if (path === root.source) - root.current = this; - else - path = root.source; - } + opacity: 0 - anchors.fill: parent - - opacity: 0 - scale: Wallpapers.showPreview ? 1 : 0.8 - - onStatusChanged: { - if (status === Image.Ready) - root.current = this; - } - - states: State { - name: "visible" - when: root.current === img - - PropertyChanges { - img.opacity: 1 - img.scale: 1 + onStatusChanged: { + if (status === Image.Ready) + anim.start(); } - } - transitions: Transition { - Anim { - target: img - properties: "opacity,scale" + Anim on opacity { + id: anim + + type: Anim.SlowEffects + running: false + from: 0 + to: 1 + } + + Timer { + running: root.current !== img && root.current?.status === Image.Ready + interval: anim.duration + onTriggered: img.destroy() } } } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index a2ed060e..db648a9c 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -102,7 +102,7 @@ ColumnLayout { } } - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Repeater { id: repeater diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 4fde197a..51794b08 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -18,7 +18,7 @@ Item { readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) - readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) + readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness) readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) @@ -57,7 +57,6 @@ Item { Anim { target: root property: "implicitWidth" - type: Anim.DefaultSpatial } }, Transition { diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index f39aa4c3..8157e118 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -86,8 +86,7 @@ Item { id: metrics text: root.windowTitle - font.pointSize: root.Tokens.font.size.smaller - font.family: root.Tokens.font.family.mono + font: root.Tokens.font.body.builders.small.letterSpacing(1.4).build() elide: Qt.ElideRight elideWidth: root.maxHeight - icon.height @@ -100,9 +99,7 @@ Item { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } component Title: StyledText { @@ -112,8 +109,7 @@ Item { anchors.top: icon.bottom anchors.topMargin: Tokens.spacing.small - font.pointSize: metrics.font.pointSize - font.family: metrics.font.family + font: metrics.font color: root.colour opacity: root.current === this ? 1 : 0 @@ -132,7 +128,9 @@ Item { height: implicitWidth Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index ffe599af..7e86ac47 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Layouts import Caelestia.Config import qs.components import qs.services @@ -9,7 +10,8 @@ StyledRect { id: root readonly property color colour: Colours.palette.m3tertiary - readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small + readonly property int padding: Config.bar.clock.background ? Tokens.padding.medium : Tokens.padding.extraSmall + readonly property var font: Tokens.font.body.builders.small.scale(1.1) implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: layout.implicitHeight + root.padding * 2 @@ -17,16 +19,15 @@ StyledRect { color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Tokens.rounding.full - Column { + ColumnLayout { id: layout anchors.centerIn: parent spacing: Tokens.spacing.small Loader { + Layout.alignment: Qt.AlignHCenter asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter - active: Config.bar.clock.showIcon visible: active @@ -37,35 +38,69 @@ StyledRect { } StyledText { - anchors.horizontalCenter: parent.horizontalCenter - + Layout.alignment: Qt.AlignHCenter visible: Config.bar.clock.showDate horizontalAlignment: StyledText.AlignHCenter text: Time.format("ddd\nd") - font.pointSize: Tokens.font.size.smaller - font.family: Tokens.font.family.sans + font: Tokens.font.body.small color: root.colour } Rectangle { - anchors.horizontalCenter: parent.horizontalCenter + Layout.fillWidth: true visible: Config.bar.clock.showDate - height: visible ? 1 : 0 - - width: parent.width * 0.8 - color: root.colour - opacity: 0.2 + implicitHeight: 1 + color: Colours.palette.m3outlineVariant } StyledText { - anchors.horizontalCenter: parent.horizontalCenter - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format(GlobalConfig.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Tokens.font.size.smaller - font.family: Tokens.font.family.mono + Layout.alignment: Qt.AlignHCenter + text: Time.hourStr + font: { + const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / hourMetrics.width); + return root.font.width(scale * 100).letterSpacing(scale).build(); + } color: root.colour + + TextMetrics { + id: hourMetrics + + font: root.font.build() + text: Time.hourStr + } + } + + StyledText { + Layout.topMargin: -parent.spacing - 4 + Layout.alignment: Qt.AlignHCenter + text: Time.minuteStr + font: { + const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / minMetrics.width); + return root.font.width(scale * 100).letterSpacing(scale).build(); + } + color: root.colour + + TextMetrics { + id: minMetrics + + font: root.font.build() + text: Time.minuteStr + } + } + + Loader { + Layout.topMargin: -parent.spacing - 4 + Layout.alignment: Qt.AlignHCenter + asynchronous: true + active: GlobalConfig.services.useTwelveHourClock + visible: active + + sourceComponent: StyledText { + text: Time.amPmStr.toLowerCase() + font: Tokens.font.body.builders.small.scale(0.9).build() + color: root.colour + } } } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 94d1a1c4..99314470 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -8,8 +8,8 @@ import qs.utils Item { id: root - implicitWidth: Math.round(Tokens.font.size.large * 1.2) - implicitHeight: Math.round(Tokens.font.size.large * 1.2) + implicitWidth: Math.round(Tokens.font.body.large.pointSize * 1.2) + implicitHeight: Math.round(Tokens.font.body.large.pointSize * 1.2) MouseArea { anchors.fill: parent @@ -30,8 +30,8 @@ Item { id: caelestiaLogo Logo { - implicitWidth: Math.round(Tokens.font.size.large * 1.6) - implicitHeight: Math.round(Tokens.font.size.large * 1.6) + implicitWidth: Math.round(Tokens.font.body.large.pointSize * 1.6) + implicitHeight: Math.round(Tokens.font.body.large.pointSize * 1.6) } } @@ -40,7 +40,7 @@ Item { ColouredIcon { source: SysInfo.osLogo - implicitSize: Math.round(Tokens.font.size.large * 1.2) + implicitSize: Math.round(Tokens.font.body.large.pointSize * 1.2) colour: Colours.palette.m3tertiary } } diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 681a805d..ba810f8b 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -8,7 +8,7 @@ Item { required property DrawerVisibilities visibilities - implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small implicitHeight: icon.implicitHeight StateLayer { @@ -16,7 +16,7 @@ Item { anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full onClicked: root.visibilities.session = !root.visibilities.session } @@ -29,7 +29,6 @@ Item { text: "power_settings_new" color: Colours.palette.m3error - font.bold: true - font.pointSize: Tokens.font.size.normal + fontStyle: Tokens.font.icon.builders.small.weight(Font.Bold).build() } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 900e5574..539a07f2 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -21,7 +21,7 @@ StyledRect { clip: true implicitWidth: Tokens.sizes.bar.innerWidth - implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) + implicitHeight: iconColumn.implicitHeight + Tokens.padding.medium * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ColumnLayout { id: iconColumn @@ -29,9 +29,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: Tokens.padding.normal + anchors.bottomMargin: Tokens.padding.medium - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 // Lock keys status WrappedLoader { @@ -57,7 +57,9 @@ StyledRect { color: root.colour Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -88,7 +90,9 @@ StyledRect { color: root.colour Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -136,7 +140,7 @@ StyledRect { animate: true text: Hypr.kbLayout color: root.colour - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium } } @@ -172,7 +176,7 @@ StyledRect { active: Config.bar.status.showBluetooth sourceComponent: ColumnLayout { - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 // Bluetooth icon MaterialIcon { @@ -245,15 +249,7 @@ StyledRect { return "rocket_launch"; return "balance"; } - - const perc = UPower.displayDevice.percentage; - const charging = [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); - if (perc === 1) - return charging ? "battery_charging_full" : "battery_full"; - let level = Math.floor(perc * 7); - if (charging && (level === 4 || level === 1)) - level--; - return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + return Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)); } color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error fill: 1 diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 85a920c1..4545d554 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -14,7 +14,7 @@ StyledRect { readonly property alias items: items readonly property alias expandIcon: expandIcon - readonly property int padding: Config.bar.tray.background ? Tokens.padding.normal : Tokens.padding.small + readonly property int padding: Config.bar.tray.background ? Tokens.padding.medium : Tokens.padding.extraSmall readonly property int spacing: Config.bar.tray.background ? Tokens.spacing.small : 0 property bool expanded @@ -75,7 +75,9 @@ StyledRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -91,16 +93,16 @@ StyledRect { sourceComponent: Item { implicitWidth: expandIconInner.implicitWidth - implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small * 2 + implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small MaterialIcon { id: expandIconInner anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.small : -Tokens.padding.small + anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.extraSmall : -Tokens.padding.extraSmall text: "expand_less" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large rotation: root.expanded ? 180 : 0 Behavior on rotation { @@ -115,8 +117,6 @@ StyledRect { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index fefb532c..5bc9f993 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -13,8 +13,8 @@ MouseArea { required property SystemTrayItem modelData acceptedButtons: Qt.LeftButton | Qt.RightButton - implicitWidth: Tokens.font.size.small * 2 - implicitHeight: Tokens.font.size.small * 2 + implicitWidth: Tokens.font.body.small.pointSize * 2 + implicitHeight: Tokens.font.body.small.pointSize * 2 onClicked: event => { if (event.button === Qt.LeftButton) diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index ebc0caf2..ad279498 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -44,7 +44,7 @@ StyledRect { clip: true y: offset + mask.y - implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small implicitHeight: size radius: Tokens.rounding.full color: Colours.palette.m3primary diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 2bd3c8cb..d971940a 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -65,7 +65,7 @@ Item { anchors.horizontalCenter: root.horizontalCenter y: (start?.y ?? 0) - 1 - implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small + 2 implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 3ed382e9..70476df8 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -18,7 +18,7 @@ Item { readonly property string activeSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" layer.enabled: true - layer.effect: OpacityMask { + layer.effect: Mask { maskSource: mask } @@ -65,7 +65,9 @@ Item { opacity: view.contentY > 0 ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -76,10 +78,12 @@ Item { radius: Tokens.rounding.full implicitHeight: parent.height / 2 - opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.small ? 0 : 1 + opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.extraSmall ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -88,7 +92,7 @@ Item { id: view anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium interactive: false currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) @@ -213,7 +217,7 @@ Item { drag.target: view.contentItem drag.axis: Drag.YAxis drag.maximumY: 0 - drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.small) + drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.extraSmall) onPressed: event => startY = event.y @@ -233,7 +237,7 @@ Item { id: ws required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.small : 0) + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.extraSmall : 0) property int wsId property string icon property bool hasWindows @@ -284,7 +288,7 @@ Item { asynchronous: true Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small sourceComponent: ws.icon.length === 1 ? letterComp : iconComp diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index bd581aab..51e54255 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -18,7 +18,7 @@ ColumnLayout { readonly property bool isWorkspace: true // Flag for finding workspace children // Unanimated prop for others to use as reference - readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.small : 0) + readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.extraSmall : 0) readonly property int ws: groupOffset + index + 1 readonly property bool isOccupied: occupied[ws] ?? false @@ -33,7 +33,7 @@ ColumnLayout { id: indicator Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small animate: true text: { diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index 2030bf80..2120aef9 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -28,7 +28,7 @@ StyledClippingRect { property real blur: onSpecial ? 1 : 0 implicitWidth: Tokens.sizes.bar.innerWidth - implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.small color: Colours.tPalette.m3surfaceContainer radius: Tokens.rounding.full @@ -51,7 +51,7 @@ StyledClippingRect { active: Config.bar.workspaces.occupiedBg anchors.fill: parent - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall sourceComponent: OccupiedBg { workspaces: workspaces @@ -64,7 +64,7 @@ StyledClippingRect { id: layout anchors.centerIn: parent - spacing: Math.floor(Tokens.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.extraSmall) Repeater { id: workspaces @@ -108,7 +108,9 @@ StyledClippingRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -118,7 +120,7 @@ StyledClippingRect { asynchronous: true anchors.fill: parent - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall active: opacity > 0 @@ -134,7 +136,9 @@ StyledClippingRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 12246645..dc2ef8cf 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -12,21 +12,21 @@ Item { required property PopoutState popouts - implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.large * 2 + implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.extraLargeIncreased implicitHeight: child.implicitHeight Column { id: child anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { id: detailsRow anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium IconImage { id: icon @@ -46,7 +46,7 @@ Item { StyledText { Layout.fillWidth: true text: Hypr.activeToplevel?.title ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } @@ -59,13 +59,13 @@ Item { } Item { - implicitWidth: expandIcon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: expandIcon.implicitHeight + Tokens.padding.small * 2 + implicitWidth: expandIcon.implicitHeight + Tokens.padding.small + implicitHeight: expandIcon.implicitHeight + Tokens.padding.small Layout.alignment: Qt.AlignVCenter StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.popouts.detachRequested("winfo") } @@ -77,14 +77,14 @@ Item { text: "chevron_right" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } ClippingWrapperRectangle { color: "transparent" - radius: Tokens.rounding.small + radius: Tokens.rounding.medium ScreencopyView { id: preview diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index e8dc9203..18983428 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -14,8 +14,8 @@ Item { required property PopoutState popouts - implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: layout.implicitHeight + Tokens.padding.normal * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.medium * 2 ButtonGroup { id: sinks @@ -30,11 +30,11 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Output device") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Repeater { @@ -53,9 +53,9 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.smaller + Layout.topMargin: Tokens.spacing.medium text: qsTr("Input device") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Repeater { @@ -72,15 +72,14 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.smaller - Layout.bottomMargin: -Tokens.spacing.small / 2 + Layout.topMargin: Tokens.spacing.medium text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } CustomMouseArea { Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 + implicitHeight: Tokens.padding.medium * 3 onWheel: event => { if (event.angleDelta.y > 0) @@ -95,20 +94,16 @@ Item { implicitHeight: parent.implicitHeight value: Audio.volume - onMoved: Audio.setVolume(value) - - Behavior on value { - Anim {} - } + onInteraction: value => Audio.setVolume(value) } } IconTextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Tokens.padding.small + verticalPadding: Tokens.padding.extraSmall text: qsTr("Open settings") icon: "settings" diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 93d2012b..3c3ced51 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -9,7 +9,7 @@ import qs.services Column { id: root - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium width: Tokens.sizes.bar.batteryWidth StyledText { @@ -45,11 +45,11 @@ Column { height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { - implicitWidth: child.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: child.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: child.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: child.implicitHeight + Tokens.padding.large color: Colours.palette.m3error - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Column { id: child @@ -72,8 +72,7 @@ Column { anchors.verticalCenter: parent.verticalCenter text: qsTr("Performance Degraded") color: Colours.palette.m3onError - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.mono.builders.medium.weight(Font.Medium).build() } MaterialIcon { @@ -109,8 +108,8 @@ Column { anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.normal * 2 + Tokens.spacing.large * 2 - implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small * 2 + implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.medium * 2 + Tokens.spacing.largeIncreased * 2 + implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small color: Colours.tPalette.m3surfaceContainer radius: Tokens.rounding.full @@ -147,9 +146,7 @@ Column { ] transitions: Transition { - AnchorAnim { - type: AnchorAnim.Emphasized - } + AnchorAnim {} } } @@ -158,7 +155,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Tokens.padding.small + anchors.leftMargin: Tokens.padding.extraSmall profile: PowerProfile.PowerSaver icon: "energy_savings_leaf" @@ -178,7 +175,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Tokens.padding.small + anchors.rightMargin: Tokens.padding.extraSmall profile: PowerProfile.Performance icon: "rocket_launch" @@ -199,8 +196,8 @@ Column { required property string icon required property int profile - implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small + implicitHeight: icon.implicitHeight + Tokens.padding.small StateLayer { radius: Tokens.rounding.full @@ -214,12 +211,14 @@ Column { anchors.centerIn: parent text: parent.icon - font.pointSize: Tokens.font.size.large - color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + fontStyle: Tokens.font.icon.large + color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurfaceVariant fill: profiles.current === text ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index baca115e..ebf21de3 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -19,10 +19,10 @@ ColumnLayout { spacing: Tokens.spacing.small StyledText { - Layout.topMargin: Tokens.padding.normal - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.padding.medium + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Bluetooth") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Toggle { @@ -47,7 +47,7 @@ ColumnLayout { StyledText { Layout.topMargin: Tokens.spacing.small - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall text: { const devices = Bluetooth.devices.values; // qmllint disable unresolved-type let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); @@ -57,7 +57,7 @@ ColumnLayout { return available; } color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Repeater { @@ -72,7 +72,7 @@ ColumnLayout { readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall spacing: Tokens.spacing.small opacity: 0 @@ -84,7 +84,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -96,8 +98,8 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Tokens.spacing.small / 2 - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall Layout.fillWidth: true text: device.modelData.name elide: Text.ElideRight @@ -105,15 +107,15 @@ ColumnLayout { MaterialIcon { visible: device.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type - text: Icons.getBatteryIcon(device.modelData.batteryAvailable ? device.modelData.battery * 100 : -1) - color: device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + text: device.modelData.batteryAvailable ? Icons.getBatteryIcon(device.modelData.battery) : "battery_alert" + color: device.modelData.batteryAvailable && device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant } StyledRect { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.extraSmall radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type @@ -140,7 +142,9 @@ ColumnLayout { opacity: device.loading ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -169,10 +173,10 @@ ColumnLayout { IconTextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Tokens.padding.small + verticalPadding: Tokens.padding.extraSmall text: qsTr("Open settings") icon: "settings" @@ -185,8 +189,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small - spacing: Tokens.spacing.normal + Layout.rightMargin: Tokens.padding.extraSmall + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index ab80d2cc..99785219 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -33,9 +33,7 @@ Item { } Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on x { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 42c5a766..a3d776bf 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -14,8 +14,8 @@ Item { readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null - implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.large * 2 - implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.large * 2 + implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.extraLargeIncreased + implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.extraLargeIncreased Item { id: content @@ -172,7 +172,6 @@ Item { anchors.centerIn: parent opacity: 0 - scale: 0.8 active: false states: State { @@ -182,7 +181,6 @@ Item { PropertyChanges { popout.active: true popout.opacity: 1 - popout.scale: 1 } } @@ -193,11 +191,10 @@ Item { SequentialAnimation { Anim { - properties: "opacity,scale" - type: Anim.StandardSmall + property: "opacity" + type: Anim.DefaultEffects } PropertyAction { - target: popout property: "active" } } @@ -208,11 +205,11 @@ Item { SequentialAnimation { PropertyAction { - target: popout property: "active" } Anim { - properties: "opacity,scale" + property: "opacity" + type: Anim.SlowEffects } } } diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 63b69b57..f8d8247d 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -26,10 +26,10 @@ ColumnLayout { StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Tokens.padding.normal : 0 - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: visible ? Tokens.padding.medium : 0 + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Wireless") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Toggle { @@ -44,10 +44,10 @@ ColumnLayout { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Tokens.spacing.small : 0 - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Repeater { @@ -70,7 +70,7 @@ ColumnLayout { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall spacing: Tokens.spacing.small opacity: 0 @@ -82,7 +82,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -97,22 +99,22 @@ ColumnLayout { MaterialIcon { visible: networkItem.modelData.isSecure text: "lock" - font.pointSize: Tokens.font.size.small + fontStyle: Tokens.font.icon.small } StyledText { - Layout.leftMargin: Tokens.spacing.small / 2 - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall Layout.fillWidth: true text: networkItem.modelData.ssid elide: Text.ElideRight - font.weight: networkItem.modelData.active ? 500 : 400 + font: Tokens.font.body.builders.medium.weight(networkItem.modelData.active ? Font.Medium : Font.Normal).build() color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface } StyledRect { implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.small + implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.extraSmall radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) @@ -155,7 +157,9 @@ ColumnLayout { opacity: networkItem.loading ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -167,7 +171,7 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Tokens.spacing.small : 0 Layout.fillWidth: true - implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small * 2 + implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer @@ -201,15 +205,17 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } CircularIndicator { anchors.centerIn: parent - strokeWidth: Tokens.padding.small / 2 + strokeWidth: Tokens.padding.extraSmall / 2 bgColour: "transparent" - implicitSize: parent.implicitHeight - Tokens.padding.smaller * 2 + implicitSize: parent.implicitHeight - Tokens.padding.large running: Nmcli.scanning } } @@ -218,20 +224,20 @@ ColumnLayout { StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Tokens.padding.normal : 0 - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: visible ? Tokens.padding.medium : 0 + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Ethernet") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Tokens.spacing.small : 0 - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Repeater { @@ -253,7 +259,7 @@ ColumnLayout { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall spacing: Tokens.spacing.small opacity: 0 @@ -265,7 +271,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -278,18 +286,18 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Tokens.spacing.small / 2 - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.extraSmall + Layout.rightMargin: Tokens.spacing.extraSmall Layout.fillWidth: true text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight - font.weight: ethernetItem.modelData.connected ? 500 : 400 + font: Tokens.font.body.builders.medium.weight(ethernetItem.modelData.connected ? Font.Medium : Font.Normal).build() color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface } StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.extraSmall radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) @@ -323,7 +331,9 @@ ColumnLayout { opacity: ethernetItem.loading ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -371,8 +381,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small - spacing: Tokens.spacing.normal + Layout.rightMargin: Tokens.padding.extraSmall + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 7975d1bf..c4b8bf2c 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -45,7 +45,7 @@ StackView { property bool isSubMenu property bool shown - padding: Tokens.padding.smaller + padding: Tokens.padding.small spacing: Tokens.spacing.small opacity: shown ? 1 : 0 @@ -57,7 +57,9 @@ StackView { StackView.onRemoved: destroy() Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -97,9 +99,9 @@ StackView { implicitHeight: label.implicitHeight StateLayer { - anchors.margins: -Tokens.padding.small / 2 - anchors.leftMargin: -Tokens.padding.smaller - anchors.rightMargin: -Tokens.padding.smaller + anchors.margins: -Tokens.padding.extraSmall / 2 + anchors.leftMargin: -Tokens.padding.small + anchors.rightMargin: -Tokens.padding.small radius: item.radius disabled: !item.modelData.enabled @@ -138,7 +140,7 @@ StackView { id: label anchors.left: icon.right - anchors.leftMargin: icon.active ? Tokens.spacing.smaller : 0 + anchors.leftMargin: icon.active ? Tokens.spacing.medium : 0 text: labelMetrics.elidedText color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -148,11 +150,10 @@ StackView { id: labelMetrics text: item.modelData.text - font.pointSize: label.font.pointSize - font.family: label.font.family + font: label.font elide: Text.ElideRight - elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.normal : 0) + elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.medium : 0) } Loader { @@ -180,7 +181,7 @@ StackView { sourceComponent: Item { implicitWidth: back.implicitWidth - implicitHeight: back.implicitHeight + Tokens.spacing.small / 2 + implicitHeight: back.implicitHeight + Tokens.spacing.extraSmall Item { anchors.bottom: parent.bottom @@ -189,9 +190,9 @@ StackView { StyledRect { anchors.fill: parent - anchors.margins: -Tokens.padding.small / 2 - anchors.leftMargin: -Tokens.padding.smaller - anchors.rightMargin: -Tokens.padding.smaller * 2 + anchors.margins: -Tokens.padding.extraSmall / 2 + anchors.leftMargin: -Tokens.padding.small + anchors.rightMargin: -Tokens.padding.large radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index f6a635fe..40d159df 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -69,9 +69,9 @@ ColumnLayout { } } - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium implicitWidth: 400 - implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + implicitHeight: content.implicitHeight + Tokens.padding.extraLargeIncreased visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled @@ -128,8 +128,8 @@ ColumnLayout { StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 - implicitHeight: content.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: content.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 @@ -137,7 +137,9 @@ ColumnLayout { Keys.onEscapePressed: root.closeDialog() Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -153,6 +155,7 @@ ColumnLayout { } Anim { + type: Anim.DefaultEffects target: parent property: "opacity" to: 0 @@ -172,19 +175,18 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -201,7 +203,7 @@ ColumnLayout { return qsTr("Network: Unknown"); } color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Timer { @@ -249,10 +251,9 @@ ColumnLayout { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - font.weight: 400 + font: Tokens.font.body.builders.small.weight(Font.Normal).build() wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Tokens.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.extraLargeIncreased } FocusScope { @@ -261,9 +262,9 @@ ColumnLayout { property string passwordBuffer: "" objectName: "passwordContainer" - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.medium * 2) focus: true activeFocusOnTab: true @@ -336,7 +337,7 @@ ColumnLayout { StyledRect { anchors.fill: parent - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) border.color: { @@ -365,7 +366,7 @@ ColumnLayout { StateLayer { hoverEnabled: false cursorShape: Qt.IBeamCursor - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: passwordContainer.forceActiveFocus() } @@ -375,12 +376,13 @@ ColumnLayout { anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -391,10 +393,10 @@ ColumnLayout { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Tokens.font.size.normal + implicitHeight: Tokens.font.body.medium.pointSize orientation: Qt.Horizontal - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall interactive: false model: ScriptModel { @@ -408,7 +410,7 @@ ColumnLayout { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Tokens.rounding.small / 2 + radius: Tokens.rounding.medium / 2 opacity: 0 scale: 0 @@ -428,6 +430,7 @@ ColumnLayout { } ParallelAnimation { Anim { + type: Anim.DefaultEffects target: ch property: "opacity" to: 0 @@ -446,7 +449,9 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -463,15 +468,15 @@ ColumnLayout { } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -486,7 +491,7 @@ ColumnLayout { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 7a66d3b9..d360f30d 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -185,6 +185,7 @@ Item { property: "active" } Anim { + type: Anim.DefaultEffects property: "opacity" } } @@ -195,6 +196,7 @@ Item { SequentialAnimation { Anim { + type: Anim.DefaultEffects property: "opacity" } PropertyAction { diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index d8f9a462..abbbae09 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -24,10 +24,10 @@ ColumnLayout { } StyledText { - Layout.topMargin: Tokens.padding.normal - Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.padding.medium + Layout.rightMargin: Tokens.padding.extraSmall text: qsTr("Keyboard Layouts") - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } ListView { @@ -36,7 +36,7 @@ ColumnLayout { model: kb.visibleModel Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall Layout.topMargin: Tokens.spacing.small clip: true @@ -88,7 +88,7 @@ ColumnLayout { readonly property bool isDisabled: layoutIndex > 3 width: list.width - height: Math.max(36, rowText.implicitHeight + Tokens.padding.small * 2) + height: Math.max(36, rowText.implicitHeight + Tokens.padding.small) ToolTip.visible: isDisabled && layer.containsMouse ToolTip.text: "XKB limitation: maximum 4 layouts allowed" @@ -114,8 +114,8 @@ ColumnLayout { anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right - anchors.leftMargin: Tokens.padding.small - anchors.rightMargin: Tokens.padding.small + anchors.leftMargin: Tokens.padding.extraSmall + anchors.rightMargin: Tokens.padding.extraSmall text: kbDelegate.label elide: Text.ElideRight opacity: kbDelegate.isDisabled ? 0.4 : 1.0 @@ -126,7 +126,7 @@ ColumnLayout { Rectangle { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall Layout.topMargin: Tokens.spacing.small implicitHeight: 1 @@ -139,7 +139,7 @@ ColumnLayout { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Tokens.padding.small + Layout.rightMargin: Tokens.padding.extraSmall Layout.topMargin: Tokens.spacing.small spacing: Tokens.spacing.small @@ -155,7 +155,7 @@ ColumnLayout { Layout.fillWidth: true text: kb.activeLabel elide: Text.ElideRight - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() color: Colours.palette.m3primary } diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index c8b435ca..57bea637 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -12,7 +12,7 @@ Item { id: root required property ShellScreen screen - readonly property int rounding: floating ? 0 : Tokens.rounding.large + readonly property int rounding: floating ? 0 : Tokens.rounding.extraLarge property alias floating: session.floating property alias active: session.active diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index a126c800..85a367e4 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -15,16 +15,16 @@ Item { required property Session session required property bool initialOpeningComplete - implicitWidth: layout.implicitWidth + Tokens.padding.larger * 4 - implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.extraExtraLarge + implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased ColumnLayout { id: layout anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.padding.larger * 2 - spacing: Tokens.spacing.normal + anchors.leftMargin: Tokens.padding.medium * 2 + spacing: Tokens.spacing.medium states: State { name: "expanded" @@ -42,7 +42,7 @@ Item { } Loader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased asynchronous: true active: !root.session.floating visible: active @@ -51,10 +51,10 @@ Item { readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 implicitWidth: nonAnimWidth - implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.normal * 2 : nonAnimWidth + implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.medium * 2 : nonAnimWidth color: Colours.palette.m3primaryContainer - radius: Tokens.rounding.small + radius: Tokens.rounding.medium StateLayer { id: normalWinState @@ -79,7 +79,7 @@ Item { text: "select_window" color: Colours.palette.m3onPrimaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: 1 } @@ -88,7 +88,7 @@ Item { anchors.left: normalWinIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium text: qsTr("Float window") color: Colours.palette.m3onPrimaryContainer @@ -102,15 +102,11 @@ Item { } Behavior on implicitWidth { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } @@ -121,7 +117,7 @@ Item { NavItem { required property int index - Layout.topMargin: index === 0 ? Tokens.spacing.large * 2 : 0 + Layout.topMargin: index === 0 ? Tokens.spacing.largeIncreased * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label } @@ -146,7 +142,7 @@ Item { expandedLabel.opacity: 1 smallLabel.opacity: 0 background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth - background.implicitHeight: icon.implicitHeight + root.Tokens.padding.normal * 2 + background.implicitHeight: icon.implicitHeight + root.Tokens.padding.medium * 2 item.implicitHeight: background.implicitHeight } } @@ -159,7 +155,6 @@ Item { Anim { properties: "implicitWidth,implicitHeight" - type: Anim.DefaultSpatial } } @@ -170,7 +165,7 @@ Item { color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 - implicitHeight: icon.implicitHeight + Tokens.padding.small + implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall StateLayer { onClicked: { @@ -193,11 +188,13 @@ Item { text: item.icon color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: item.active ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -206,12 +203,12 @@ Item { anchors.left: icon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium opacity: 0 text: item.label color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.capitalization: Font.Capitalize + font: Tokens.font.label.builders.medium.capitalisation(Font.Capitalize).build() } StyledText { @@ -219,11 +216,10 @@ Item { anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom - anchors.topMargin: Tokens.spacing.small / 2 + anchors.topMargin: Tokens.spacing.extraSmall text: item.label - font.pointSize: Tokens.font.size.small - font.capitalization: Font.Capitalize + font: Tokens.font.label.builders.small.capitalisation(Font.Capitalize).build() } } } diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index 8f66968b..143168a2 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -10,7 +10,7 @@ StyledRect { required property ShellScreen screen required property Session session - implicitHeight: text.implicitHeight + Tokens.padding.normal + implicitHeight: text.implicitHeight + Tokens.padding.medium color: Colours.tPalette.m3surfaceContainer StyledText { @@ -20,18 +20,16 @@ StyledRect { anchors.bottom: parent.bottom text: qsTr("Caelestia Settings - %1").arg(root.session.active) - font.capitalization: Font.Capitalize - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.title.builders.medium.capitalisation(Font.Capitalize).weight(Font.Medium).build() } Item { anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium implicitWidth: implicitHeight - implicitHeight: closeIcon.implicitHeight + Tokens.padding.small + implicitHeight: closeIcon.implicitHeight + Tokens.padding.extraSmall StateLayer { onClicked: { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index a1e0e42c..33676862 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -109,10 +109,9 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: Tokens.spacing.normal + Layout.bottomMargin: Tokens.spacing.medium text: qsTr("Wallpaper") - font.pointSize: Tokens.font.size.extraLarge - font.weight: 600 + font: Tokens.font.body.builders.large.size(28).weight(Font.DemiBold).build() } Loader { @@ -120,7 +119,7 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.bottomMargin: -Tokens.padding.large * 2 + Layout.bottomMargin: -Tokens.padding.extraLargeIncreased asynchronous: true active: { @@ -173,12 +172,11 @@ Item { spacing: Tokens.spacing.small RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Appearance") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } Item { diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index 412441ab..921d3abe 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 3f31f10b..75018df2 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -37,10 +37,9 @@ CollapsibleSection { } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Desktop Clock") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } SwitchRow { @@ -69,8 +68,7 @@ CollapsibleSection { StyledText { text: qsTr("Positioning") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } SplitButtonRow { @@ -160,8 +158,7 @@ CollapsibleSection { StyledText { text: qsTr("Shadow") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } SwitchRow { @@ -174,7 +171,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -199,7 +196,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -229,8 +226,7 @@ CollapsibleSection { StyledText { text: qsTr("Background") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } SwitchRow { @@ -252,7 +248,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -278,10 +274,9 @@ CollapsibleSection { } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Visualiser") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } SwitchRow { @@ -303,7 +298,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -328,7 +323,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index 7dbd2dbe..71bb6faf 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -43,7 +43,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 0a65cbdc..5841d05c 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -18,7 +18,7 @@ CollapsibleSection { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall Repeater { model: Schemes.list @@ -32,10 +32,10 @@ CollapsibleSection { readonly property bool isCurrent: schemeKey === Schemes.currentScheme color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: schemeRow.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -65,9 +65,9 @@ CollapsibleSection { id: schemeRow anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { id: preview @@ -87,7 +87,7 @@ CollapsibleSection { visible: false text: "circle" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } Item { @@ -116,12 +116,12 @@ CollapsibleSection { StyledText { text: modelData.flavour ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { text: modelData.name ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -137,7 +137,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index c612485d..3d0198aa 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -18,7 +18,7 @@ CollapsibleSection { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall Repeater { model: M3Variants.list @@ -29,10 +29,10 @@ CollapsibleSection { Layout.fillWidth: true color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: variantRow.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -62,27 +62,27 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: modelData.icon - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: modelData.variant === Schemes.currentVariant ? 1 : 0 } StyledText { Layout.fillWidth: true text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + font: modelData.variant === Schemes.currentVariant ? Tokens.font.body.builders.small.weight(Font.Medium).build() : Tokens.font.body.builders.small.weight(Font.Normal).build() } MaterialIcon { visible: modelData.variant === Schemes.currentVariant text: "check" color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 10040c58..03bec48f 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -38,7 +38,7 @@ CollapsibleSection { property alias contentHeight: sansFontList.contentHeight clip: true - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { @@ -52,10 +52,10 @@ CollapsibleSection { width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -70,13 +70,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: modelData - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } Item { @@ -90,7 +90,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } @@ -119,7 +119,7 @@ CollapsibleSection { property alias contentHeight: monoFontList.contentHeight clip: true - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { @@ -133,10 +133,10 @@ CollapsibleSection { width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -151,13 +151,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: modelData - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } Item { @@ -171,7 +171,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } @@ -202,7 +202,7 @@ CollapsibleSection { property alias contentHeight: materialFontList.contentHeight clip: true - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) StyledScrollBar.vertical: StyledScrollBar { @@ -216,10 +216,10 @@ CollapsibleSection { width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -234,13 +234,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: modelData - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } Item { @@ -254,7 +254,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } @@ -264,7 +264,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index dac6226e..60e7e493 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -43,7 +43,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -67,7 +67,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index f2f15ba1..eaa8dfd6 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -28,7 +28,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -53,7 +53,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index f3edb731..1f0da38d 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -38,16 +38,15 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Audio") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } Item { @@ -72,8 +71,7 @@ Item { StyledText { text: qsTr("Devices (%1)").arg(Audio.sinks.length) - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } } @@ -93,8 +91,8 @@ Item { Layout.fillWidth: true color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Tokens.rounding.normal - implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 + radius: Tokens.rounding.large + implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -108,13 +106,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: Audio.sink?.id === modelData.id ? 1 : 0 } @@ -124,7 +122,7 @@ Item { maximumLineCount: 1 text: modelData.description || qsTr("Unknown") - font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + font: Audio.sink?.id === modelData.id ? Tokens.font.body.builders.small.weight(Font.Medium).build() : Tokens.font.body.builders.small.weight(Font.Normal).build() } } } @@ -149,8 +147,7 @@ Item { StyledText { text: qsTr("Devices (%1)").arg(Audio.sources.length) - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } } @@ -170,8 +167,8 @@ Item { Layout.fillWidth: true color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Tokens.rounding.normal - implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 + radius: Tokens.rounding.large + implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -185,13 +182,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: "mic" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: Audio.source?.id === modelData.id ? 1 : 0 } @@ -201,7 +198,7 @@ Item { maximumLineCount: 1 text: modelData.description || qsTr("Unknown") - font.weight: Audio.source?.id === modelData.id ? 500 : 400 + font: Audio.source?.id === modelData.id ? Tokens.font.body.builders.small.weight(Font.Medium).build() : Tokens.font.body.builders.small.weight(Font.Normal).build() } } } @@ -229,7 +226,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "volume_up" @@ -242,7 +239,7 @@ Item { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium ColumnLayout { Layout.fillWidth: true @@ -250,12 +247,11 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Volume") - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Item { @@ -306,15 +302,15 @@ Item { StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium opacity: Audio.muted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: muteIcon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: muteIcon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { @@ -339,7 +335,7 @@ Item { id: outputVolumeSlider Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 + implicitHeight: Tokens.padding.medium * 3 value: Audio.volume enabled: !Audio.muted @@ -360,7 +356,7 @@ Item { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium ColumnLayout { Layout.fillWidth: true @@ -368,12 +364,11 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Volume") - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } Item { @@ -424,15 +419,15 @@ Item { StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium opacity: Audio.sourceMuted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { @@ -457,7 +452,7 @@ Item { id: inputVolumeSlider Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 + implicitHeight: Tokens.padding.medium * 3 value: Audio.sourceVolume enabled: !Audio.sourceMuted @@ -478,7 +473,7 @@ Item { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium ColumnLayout { Layout.fillWidth: true @@ -493,15 +488,15 @@ Item { required property int index Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: "apps" - font.pointSize: Tokens.font.size.normal + fontStyle: Tokens.font.icon.medium fill: 0 } @@ -510,8 +505,7 @@ Item { elide: Text.ElideRight maximumLineCount: 1 text: Audio.getStreamName(modelData) - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() } StyledInputField { @@ -558,15 +552,15 @@ Item { StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { @@ -587,7 +581,7 @@ Item { StyledSlider { Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 + implicitHeight: Tokens.padding.medium * 3 value: Audio.getStreamVolume(modelData) enabled: !Audio.getStreamMuted(modelData) @@ -617,7 +611,7 @@ Item { visible: Audio.streams.length === 0 text: qsTr("No applications currently playing audio") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small horizontalAlignment: Text.AlignHCenter } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 41575226..acce9776 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -54,13 +54,12 @@ StyledFlickable { sections: [ Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Connection status") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -70,9 +69,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Tokens.padding.large * 2 + implicitHeight: deviceStatus.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -83,7 +82,7 @@ StyledFlickable { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.larger + spacing: Tokens.spacing.large Toggle { label: qsTr("Connected") @@ -113,13 +112,12 @@ StyledFlickable { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Device properties") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -129,9 +127,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Tokens.padding.large * 2 + implicitHeight: deviceProps.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -142,7 +140,7 @@ StyledFlickable { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.larger + spacing: Tokens.spacing.large RowLayout { Layout.fillWidth: true @@ -167,7 +165,7 @@ StyledFlickable { PropertyChanges { renameDevice.implicitHeight: deviceNameEdit.implicitHeight renameLabel.opacity: 0 - deviceNameEdit.padding: root.Tokens.padding.normal + deviceNameEdit.padding: root.Tokens.padding.medium } } @@ -187,7 +185,7 @@ StyledFlickable { text: qsTr("Device name") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.label.small } StyledTextField { @@ -196,7 +194,7 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.normal + anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.medium text: root.device?.name ?? "" readOnly: !root.session.bt.editingDeviceName @@ -205,11 +203,11 @@ StyledFlickable { root.device.name = text; } - leftPadding: Tokens.padding.normal - rightPadding: Tokens.padding.normal + leftPadding: Tokens.padding.medium + rightPadding: Tokens.padding.medium background: StyledRect { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingDeviceName ? 1 : 0 @@ -231,9 +229,9 @@ StyledFlickable { StyledRect { implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.large - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingDeviceName ? 1 : 0 scale: root.session.bt.editingDeviceName ? 1 : 0.5 @@ -270,9 +268,9 @@ StyledFlickable { StyledRect { implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: editIcon.implicitHeight + Tokens.padding.large - radius: root.session.bt.editingDeviceName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + radius: root.session.bt.editingDeviceName ? Tokens.rounding.medium : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { @@ -319,13 +317,12 @@ StyledFlickable { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Device information") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -335,9 +332,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceInfo.implicitHeight + Tokens.padding.large * 2 + implicitHeight: deviceInfo.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -348,7 +345,7 @@ StyledFlickable { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") @@ -357,10 +354,10 @@ StyledFlickable { RowLayout { id: batteryPercent - Layout.topMargin: Tokens.spacing.small / 2 + Layout.topMargin: Tokens.spacing.extraSmall Layout.fillWidth: true - Layout.preferredHeight: Tokens.padding.smaller - spacing: Tokens.spacing.small / 2 + Layout.preferredHeight: Tokens.padding.small + spacing: Tokens.spacing.extraSmall StyledRect { Layout.fillWidth: true @@ -382,47 +379,47 @@ StyledFlickable { } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Dbus path") } StyledText { text: root.device?.dbusPath ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("MAC address") } StyledText { text: root.device?.address ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Bonded") } StyledText { text: root.device?.bonded ? qsTr("Yes") : qsTr("No") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("System name") } StyledText { text: root.device?.deviceName ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } } @@ -435,7 +432,7 @@ StyledFlickable { ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top - anchors.bottomMargin: Tokens.padding.normal + anchors.bottomMargin: Tokens.padding.medium Repeater { id: fabMenu @@ -467,7 +464,7 @@ StyledFlickable { Layout.alignment: Qt.AlignRight - implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.larger * 2 + implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2 radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer @@ -479,7 +476,7 @@ StyledFlickable { when: root.session.bt.fabMenuOpen PropertyChanges { - fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.large * 2 + fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.extraLargeIncreased fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } @@ -544,7 +541,7 @@ StyledFlickable { id: fabMenuItemInner anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium opacity: 0 MaterialIcon { @@ -557,7 +554,7 @@ StyledFlickable { animate: true text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === "connect" ? "dis" : "un" : "") + fabMenuItem.modelData.name color: Colours.palette.m3onPrimaryContainer - font.capitalization: Font.Capitalize + font: Tokens.font.body.builders.small.capitalisation(Font.Capitalize).build() Layout.preferredWidth: implicitWidth Behavior on Layout.preferredWidth { @@ -589,7 +586,7 @@ StyledFlickable { implicitWidth: 64 implicitHeight: 64 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer states: State { @@ -600,7 +597,7 @@ StyledFlickable { fabBg.implicitWidth: 48 fabBg.implicitHeight: 48 fabBg.radius: 48 / 2 - fab.font.pointSize: Tokens.font.size.larger + fab.fontStyle: Tokens.font.icon.large } } @@ -638,7 +635,7 @@ StyledFlickable { animate: true text: root.session.bt.fabMenuOpen ? "close" : "settings" color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: 1 } } @@ -650,7 +647,7 @@ StyledFlickable { property alias toggle: toggle Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 419410ee..f5877522 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -32,12 +32,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Bluetooth") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } Item { @@ -48,9 +47,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.enabled ?? false icon: "power" accent: "Tertiary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Toggle Bluetooth") onClicked: { @@ -64,9 +63,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discoverable ?? false icon: root.smallDiscoverable ? "group_search" : "" label: root.smallDiscoverable ? "" : qsTr("Discoverable") - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Make discoverable") onClicked: { @@ -80,9 +79,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.pairable ?? false icon: "missing_controller" label: root.smallPairable ? "" : qsTr("Pairable") - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Make pairable") onClicked: { @@ -96,9 +95,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discovering ?? false icon: "bluetooth_searching" accent: "Secondary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Scan for devices") onClicked: { @@ -112,9 +111,9 @@ DeviceList { toggled: !root.session.bt.active icon: "settings" accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Bluetooth settings") onClicked: { @@ -137,10 +136,10 @@ DeviceList { readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected width: ListView.view ? ListView.view.width : undefined - implicitHeight: deviceInner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: deviceInner.implicitHeight + Tokens.padding.medium * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large StateLayer { id: stateLayer @@ -155,15 +154,15 @@ DeviceList { id: deviceInner anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { @@ -178,11 +177,13 @@ DeviceList { anchors.centerIn: parent text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: device.connected ? 1 : 0 Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -202,7 +203,7 @@ DeviceList { Layout.fillWidth: true text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } } @@ -211,7 +212,7 @@ DeviceList { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index 1202cc5b..14d2a53f 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -16,7 +16,7 @@ ColumnLayout { required property Session session - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "bluetooth" @@ -24,10 +24,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Adapter status") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -37,9 +36,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterStatus.implicitHeight + Tokens.padding.large * 2 + implicitHeight: adapterStatus.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -50,7 +49,7 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.larger + spacing: Tokens.spacing.large Toggle { label: qsTr("Powered") @@ -85,10 +84,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Adapter properties") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -98,9 +96,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterSettings.implicitHeight + Tokens.padding.large * 2 + implicitHeight: adapterSettings.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -111,11 +109,11 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.larger + spacing: Tokens.spacing.large RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -127,28 +125,28 @@ ColumnLayout { property bool expanded - implicitWidth: adapterPicker.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: adapterPicker.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: adapterPicker.implicitHeight + Tokens.padding.large StateLayer { onClicked: { adapterPickerButton.expanded = !adapterPickerButton.expanded; } - radius: Tokens.rounding.small + radius: Tokens.rounding.medium } RowLayout { id: adapterPicker anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.topMargin: Tokens.padding.smaller - anchors.bottomMargin: Tokens.padding.smaller - spacing: Tokens.spacing.normal + anchors.margins: Tokens.padding.medium + anchors.topMargin: Tokens.padding.small + anchors.bottomMargin: Tokens.padding.small + spacing: Tokens.spacing.medium StyledText { - Layout.leftMargin: Tokens.padding.small + Layout.leftMargin: Tokens.padding.extraSmall text: Bluetooth.defaultAdapter?.name ?? qsTr("None") } @@ -184,7 +182,7 @@ ColumnLayout { implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight color: Colours.palette.m3secondaryContainer - radius: Tokens.rounding.small + radius: Tokens.rounding.medium opacity: adapterPickerButton.expanded ? 1 : 0 scale: adapterPickerButton.expanded ? 1 : 0.7 @@ -206,7 +204,7 @@ ColumnLayout { required property BluetoothAdapter modelData Layout.fillWidth: true - implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: adapterInner.implicitHeight + Tokens.padding.medium * 2 StateLayer { onClicked: { @@ -223,12 +221,12 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.normal + anchors.margins: Tokens.padding.medium + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true - Layout.leftMargin: Tokens.padding.small + Layout.leftMargin: Tokens.padding.extraSmall text: adapter.modelData.name color: Colours.palette.m3onSecondaryContainer } @@ -254,9 +252,7 @@ ColumnLayout { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } @@ -264,7 +260,7 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -305,7 +301,7 @@ ColumnLayout { PropertyChanges { renameAdapter.implicitHeight: adapterNameEdit.implicitHeight renameLabel.opacity: 0 - adapterNameEdit.padding: Tokens.padding.normal + adapterNameEdit.padding: Tokens.padding.medium } } @@ -325,7 +321,7 @@ ColumnLayout { text: qsTr("Rename adapter (currently does not work)") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.label.small } StyledTextField { @@ -334,7 +330,7 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.normal + anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.medium text: root.session.bt.currentAdapter?.name ?? "" readOnly: !root.session.bt.editingAdapterName @@ -342,11 +338,11 @@ ColumnLayout { root.session.bt.editingAdapterName = false; } - leftPadding: Tokens.padding.normal - rightPadding: Tokens.padding.normal + leftPadding: Tokens.padding.medium + rightPadding: Tokens.padding.medium background: StyledRect { - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingAdapterName ? 1 : 0 @@ -368,9 +364,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.large - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingAdapterName ? 1 : 0 scale: root.session.bt.editingAdapterName ? 1 : 0.5 @@ -407,9 +403,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: editIcon.implicitHeight + Tokens.padding.large - radius: root.session.bt.editingAdapterName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + radius: root.session.bt.editingAdapterName ? Tokens.rounding.medium : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { @@ -442,10 +438,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Adapter information") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -455,9 +450,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterInfo.implicitHeight + Tokens.padding.large * 2 + implicitHeight: adapterInfo.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -468,7 +463,7 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: qsTr("Adapter state") @@ -477,29 +472,29 @@ ColumnLayout { StyledText { text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Dbus path") } StyledText { text: Bluetooth.defaultAdapter?.dbusPath ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Adapter id") } StyledText { text: Bluetooth.defaultAdapter?.adapterId ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } } @@ -510,7 +505,7 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index f782838d..1aedbbd9 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -16,8 +16,8 @@ StyledRect { property int rows: 1 // Number of rows Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: true @@ -30,12 +30,12 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { visible: root.title !== "" text: root.title - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } GridLayout { @@ -63,7 +63,7 @@ StyledRect { Layout.fillWidth: true text: modelData.label checked: _checked - toggle: false + isToggle: false type: TextButton.Tonal // Create binding in Component.onCompleted @@ -81,13 +81,13 @@ StyledRect { // Match utilities Toggles radius styling // Each button has full rounding (not connected) since they have spacing - radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal + radius: stateLayer.pressed ? Tokens.rounding.medium / 2 : internalChecked ? Tokens.rounding.medium : Tokens.rounding.large // Match utilities Toggles inactive color inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) // Adjust width similar to utilities toggles - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.small : 0) onClicked: { if (modelData.onToggled && root.rootItem && modelData.propertyName) { diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 075024f6..1dfa327f 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -30,7 +30,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Loader { id: headerLoader diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index f02cc3c0..a56a3bd0 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -47,8 +47,7 @@ ColumnLayout { StyledText { visible: root.title !== "" text: root.title - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } Loader { @@ -78,7 +77,7 @@ ColumnLayout { model: root.model delegate: root.delegate - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall interactive: false clip: false } diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 8cd493f4..a96114d3 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -21,12 +21,12 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { visible: root.label !== "" text: root.label - font.pointSize: Tokens.font.size.normal + font: Tokens.font.label.medium color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface } @@ -38,19 +38,19 @@ ColumnLayout { visible: root.readonly text: "lock" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + fontStyle: Tokens.font.icon.small } StyledText { text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface } } StyledRect { Layout.fillWidth: true - implicitHeight: Tokens.padding.normal + implicitHeight: Tokens.padding.medium radius: Tokens.rounding.full color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) opacity: root.readonly ? 0.5 : 1.0 diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index c6ccbb67..bb65976b 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -18,20 +18,18 @@ Item { id: column anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter text: root.icon - font.pointSize: Tokens.font.size.extraLarge * 3 - font.bold: true + fontStyle: Tokens.font.icon.size(84).weight(Font.Bold).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: root.title - font.pointSize: Tokens.font.size.large - font.bold: true + font: Tokens.font.title.builders.medium.weight(Font.Bold).build() } } } diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 73fd00ae..24241859 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -71,12 +71,12 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { visible: root.label !== "" text: root.label - font.pointSize: Tokens.font.size.normal + font: Tokens.font.label.medium } Item { @@ -138,7 +138,7 @@ ColumnLayout { visible: root.suffix !== "" text: root.suffix color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } @@ -146,7 +146,7 @@ ColumnLayout { id: slider Layout.fillWidth: true - implicitHeight: Tokens.padding.normal * 3 + implicitHeight: Tokens.padding.medium * 3 from: root.from to: root.to diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 64c7c9fc..8cf23d5c 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -32,9 +32,9 @@ RowLayout { id: leftClippingRect anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal / 2 + anchors.rightMargin: Tokens.padding.medium / 2 radius: leftBorder.innerRadius color: "transparent" @@ -43,9 +43,9 @@ RowLayout { id: leftLoader anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.margins: Tokens.padding.large + Tokens.padding.medium anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large + Tokens.padding.normal / 2 + anchors.rightMargin: Tokens.padding.large + Tokens.padding.medium / 2 asynchronous: true sourceComponent: root.leftContent @@ -62,7 +62,7 @@ RowLayout { id: leftBorder leftThickness: 0 - rightThickness: Tokens.padding.normal / 2 + rightThickness: Tokens.padding.medium / 2 } } @@ -76,9 +76,9 @@ RowLayout { id: rightClippingRect anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal / 2 + anchors.rightMargin: Tokens.padding.medium / 2 radius: rightBorder.innerRadius color: "transparent" @@ -87,7 +87,7 @@ RowLayout { id: rightLoader anchors.fill: parent - anchors.margins: Tokens.padding.large * 2 + anchors.margins: Tokens.padding.extraLargeIncreased asynchronous: true sourceComponent: root.rightContent @@ -103,7 +103,7 @@ RowLayout { InnerBorder { id: rightBorder - leftThickness: Tokens.padding.normal / 2 + leftThickness: Tokens.padding.medium / 2 } } } diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 44a615c6..a2378734 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -15,11 +15,11 @@ GridView { required property Session session - readonly property int minCellWidth: 200 + Tokens.spacing.normal + readonly property int minCellWidth: 200 + Tokens.spacing.medium readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) cellWidth: width / columnsCount - cellHeight: 140 + Tokens.spacing.normal + cellHeight: 140 + Tokens.spacing.medium model: Wallpapers.list @@ -33,8 +33,8 @@ GridView { required property var modelData required property int index readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Tokens.spacing.normal / 2 - readonly property real itemRadius: Tokens.rounding.normal + readonly property real itemMargin: Tokens.spacing.medium / 2 + readonly property real itemRadius: Tokens.rounding.large width: root.cellWidth height: root.cellHeight @@ -130,7 +130,7 @@ GridView { anchors.right: parent.right anchors.bottom: parent.bottom - implicitHeight: filenameText.implicitHeight + Tokens.padding.normal * 1.5 + implicitHeight: filenameText.implicitHeight + Tokens.padding.medium * 1.5 radius: 0 gradient: Gradient { @@ -190,12 +190,12 @@ GridView { MaterialIcon { anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall visible: isCurrent text: "check_circle" color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -205,13 +205,12 @@ GridView { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.leftMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 - anchors.rightMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 - anchors.bottomMargin: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.medium + Tokens.spacing.medium / 2 + anchors.rightMargin: Tokens.padding.medium + Tokens.spacing.medium / 2 + anchors.bottomMargin: Tokens.padding.medium text: modelData.name - font.pointSize: Tokens.font.size.smaller - font.weight: 500 + font: Tokens.font.body.builders.small.weight(Font.Medium).build() color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle maximumLineCount: 1 diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 6bde196a..d69d0745 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -65,9 +65,9 @@ Item { id: dashboardClippingRect anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.medium radius: dashboardBorder.innerRadius color: "transparent" @@ -76,7 +76,7 @@ Item { id: dashboardLoader anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.margins: Tokens.padding.large + Tokens.padding.medium anchors.leftMargin: Tokens.padding.large anchors.rightMargin: Tokens.padding.large @@ -89,7 +89,7 @@ Item { id: dashboardBorder leftThickness: 0 - rightThickness: Tokens.padding.normal + rightThickness: Tokens.padding.medium } Component { @@ -112,15 +112,14 @@ Item { anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Dashboard") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } } diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index be67eab2..42b67c79 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -17,7 +17,7 @@ SectionContainer { StyledText { text: qsTr("General Settings") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SwitchRow { @@ -40,7 +40,7 @@ SectionContainer { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SwitchRow { Layout.fillWidth: true diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index ea6c68b3..12562834 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower import Caelestia.Config +import Caelestia.Services import qs.components import qs.components.controls import qs.services @@ -12,8 +13,8 @@ SectionContainer { id: root required property var rootItem - // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) - readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" + // GPU toggle is hidden when type is Gpu.None (no GPU data available) + readonly property bool gpuAvailable: Gpu.type !== Gpu.None // Battery toggle is hidden when no laptop battery is present readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery @@ -22,7 +23,7 @@ SectionContainer { StyledText { text: qsTr("Performance Resources") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } ConnectedButtonGroup { diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index f2372dbe..51e5c760 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -152,12 +152,11 @@ Item { spacing: Tokens.spacing.small RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Launcher") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } Item { @@ -168,9 +167,9 @@ Item { toggled: !root.session.launcher.active icon: "settings" accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Launcher settings") onClicked: { @@ -186,10 +185,9 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.title.builders.small.weight(Font.Medium).build() } StyledText { @@ -199,7 +197,7 @@ Item { StyledRect { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.bottomMargin: Tokens.spacing.small color: Colours.layer(Colours.palette.m3surfaceContainer, 2) @@ -212,7 +210,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.medium text: "search" color: Colours.palette.m3onSurfaceVariant @@ -226,8 +224,8 @@ Item { anchors.leftMargin: Tokens.spacing.small anchors.rightMargin: Tokens.spacing.small - topPadding: Tokens.padding.normal - bottomPadding: Tokens.padding.normal + topPadding: Tokens.padding.medium + bottomPadding: Tokens.padding.medium placeholderText: qsTr("Search applications...") @@ -241,7 +239,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.medium width: searchField.text ? implicitWidth : implicitWidth / 2 opacity: { @@ -296,7 +294,7 @@ Item { Layout.fillHeight: true model: root.filteredApps - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -312,7 +310,7 @@ Item { implicitHeight: 40 color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Tokens.rounding.normal + radius: Tokens.rounding.large opacity: 0 @@ -337,9 +335,9 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium IconImage { asynchronous: true @@ -354,7 +352,7 @@ Item { StyledText { Layout.fillWidth: true text: modelData.name || modelData.entry?.name || qsTr("Unknown") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } Loader { @@ -517,12 +515,12 @@ Item { readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - Layout.topMargin: Tokens.padding.large * 2 + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased + Layout.topMargin: Tokens.padding.extraLargeIncreased visible: displayedApp === null icon: "apps" title: qsTr("Launcher Applications") @@ -530,23 +528,23 @@ Item { Item { Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - Layout.topMargin: Tokens.padding.large * 2 + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased + Layout.topMargin: Tokens.padding.extraLargeIncreased visible: displayedApp !== null implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) - implicitHeight: appIconImage.implicitHeight + Tokens.spacing.normal + appTitleText.implicitHeight + implicitHeight: appIconImage.implicitHeight + Tokens.spacing.medium + appTitleText.implicitHeight ColumnLayout { anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium IconImage { id: appIconImage asynchronous: true Layout.alignment: Qt.AlignHCenter - implicitSize: Tokens.font.size.extraLarge * 3 * 2 + implicitSize: Tokens.font.body.large.pointSize * 3 * 2 source: { const app = appDetailsLayout.displayedApp; if (!app) @@ -564,8 +562,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" - font.pointSize: Tokens.font.size.large - font.bold: true + font: Tokens.font.title.builders.medium.weight(Font.Bold).build() } } } @@ -573,9 +570,9 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: Tokens.spacing.large - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.spacing.largeIncreased + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased StyledFlickable { id: detailsFlickable @@ -594,10 +591,10 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SwitchRow { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium visible: appDetailsLayout.displayedApp !== null label: qsTr("Mark as favourite") checked: root.favouriteChecked @@ -629,7 +626,7 @@ Item { } } SwitchRow { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium visible: appDetailsLayout.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index f0181230..5d7581ea 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -15,7 +15,7 @@ ColumnLayout { required property Session session - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "apps" @@ -23,7 +23,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("General") description: qsTr("General launcher settings") } @@ -63,13 +63,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Display") description: qsTr("Display and appearance settings") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Max shown items") @@ -90,13 +90,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Prefixes") description: qsTr("Command prefix settings") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Special prefix") @@ -111,7 +111,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Fuzzy search") description: qsTr("Fuzzy search settings") } @@ -159,13 +159,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Sizes") description: qsTr("Size settings for launcher items") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Item width") @@ -192,13 +192,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Hidden apps") description: qsTr("Applications hidden from launcher") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Total hidden") diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 1daeb4a2..e6d588c2 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -43,7 +43,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Connection status") @@ -69,7 +69,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Device properties") @@ -77,7 +77,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Interface") @@ -100,7 +100,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Connection information") diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 3a947c86..38be9363 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -23,12 +23,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Settings") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } Item { @@ -39,9 +38,9 @@ DeviceList { toggled: !root.session.ethernet.active icon: "settings" accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small onClicked: { if (root.session.ethernet.active) @@ -62,10 +61,10 @@ DeviceList { readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.medium * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large StateLayer { id: stateLayer @@ -79,15 +78,15 @@ DeviceList { id: rowLayout anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { @@ -101,12 +100,14 @@ DeviceList { anchors.centerIn: parent text: "cable" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: modelData.connected ? 1 : 0 color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -124,14 +125,13 @@ DeviceList { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - font.weight: modelData.connected ? 500 : 400 + font: Tokens.font.body.builders.small.weight(modelData.connected ? Font.Medium : Font.Normal).build() elide: Text.ElideRight } } @@ -141,7 +141,7 @@ DeviceList { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index f13b6fca..00ef4452 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -15,7 +15,7 @@ ColumnLayout { required property Session session - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "cable" @@ -23,10 +23,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased text: qsTr("Ethernet devices") - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -36,9 +35,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.large * 2 + implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -49,7 +48,7 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: qsTr("Total devices") @@ -58,18 +57,18 @@ ColumnLayout { StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("Connected devices") } StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index d8700ecd..c0677d97 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -17,7 +17,7 @@ ColumnLayout { required property Session session - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "router" @@ -25,13 +25,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Ethernet") description: qsTr("Ethernet device information") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Total devices") @@ -46,7 +46,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Wireless") description: qsTr("WiFi network settings") } @@ -62,7 +62,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("VPN") description: qsTr("VPN provider settings") visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 @@ -87,8 +87,8 @@ ColumnLayout { TextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.topMargin: Tokens.spacing.medium + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 text: qsTr("⚙ Manage VPN Providers") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer @@ -100,13 +100,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Current connection") description: qsTr("Active network connection information") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Network") @@ -140,15 +140,15 @@ ColumnLayout { parent: Overlay.overlay anchors.centerIn: parent - width: Math.min(600, parent.width - Tokens.padding.large * 2) - height: Math.min(700, parent.height - Tokens.padding.large * 2) + width: Math.min(600, parent.width - Tokens.padding.extraLargeIncreased) + height: Math.min(700, parent.height - Tokens.padding.extraLargeIncreased) modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: StyledRect { color: Colours.palette.m3surface - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge } StyledFlickable { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index e02bfafb..bf2daa1a 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -43,16 +43,15 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Network") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } Item { @@ -63,9 +62,9 @@ Item { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Toggle WiFi") onClicked: { @@ -77,9 +76,9 @@ Item { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Scan for networks") onClicked: { @@ -91,9 +90,9 @@ Item { toggled: !root.session.ethernet.active && !root.session.network.active icon: "settings" accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small tooltip: qsTr("Network settings") onClicked: { diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 7ac2dc50..983cc1a1 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -37,7 +37,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Connection status") @@ -91,12 +91,12 @@ DeviceDetails { RowLayout { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - spacing: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium + spacing: Tokens.spacing.medium TextButton { Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 visible: root.providerEnabled enabled: !VPN.connecting inactiveColour: Colours.palette.m3primaryContainer @@ -147,7 +147,7 @@ DeviceDetails { TextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" text: qsTr("Open Login Page") inactiveColour: Colours.palette.m3tertiaryContainer @@ -160,10 +160,10 @@ DeviceDetails { StyledText { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" text: qsTr("Click 'Connect' to generate authentication URL") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap @@ -173,7 +173,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Provider details") @@ -181,7 +181,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Provider") @@ -260,7 +260,7 @@ DeviceDetails { parent: Overlay.overlay anchors.centerIn: parent - width: Math.min(400, parent.width - Tokens.padding.large * 2) + width: Math.min(400, parent.width - Tokens.padding.extraLargeIncreased) padding: Tokens.padding.large * 1.5 modal: true @@ -305,7 +305,7 @@ DeviceDetails { background: StyledRect { color: Colours.palette.m3surfaceContainerHigh - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge Elevation { anchors.fill: parent @@ -316,21 +316,20 @@ DeviceDetails { } contentItem: ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Edit VPN Provider") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 StyledText { text: qsTr("Display Name") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -338,7 +337,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -353,7 +352,7 @@ DeviceDetails { id: displayNameField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.displayName onTextChanged: editVpnDialog.displayName = text @@ -363,11 +362,11 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -375,7 +374,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -390,7 +389,7 @@ DeviceDetails { id: interfaceNameField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.interfaceName onTextChanged: editVpnDialog.interfaceName = text @@ -400,12 +399,12 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 visible: editVpnDialog.connectCmd.length > 0 StyledText { text: qsTr("Connect Command") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -413,7 +412,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -428,7 +427,7 @@ DeviceDetails { id: connectCmdFieldEdit anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.connectCmd onTextChanged: editVpnDialog.connectCmd = text @@ -438,12 +437,12 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 visible: editVpnDialog.disconnectCmd.length > 0 StyledText { text: qsTr("Disconnect Command") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -451,7 +450,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -466,7 +465,7 @@ DeviceDetails { id: disconnectCmdFieldEdit anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.disconnectCmd onTextChanged: editVpnDialog.disconnectCmd = text @@ -475,9 +474,9 @@ DeviceDetails { } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { Layout.fillWidth: true diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index d9266b34..b1b7b01b 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -18,7 +18,7 @@ ColumnLayout { property bool showHeader: true property int pendingSwitchIndex: -1 - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Connections { function onConnectedChanged() { @@ -76,7 +76,7 @@ ColumnLayout { Layout.preferredHeight: contentHeight interactive: false - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium model: ScriptModel { values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { @@ -103,10 +103,10 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.medium * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large StateLayer { onClicked: { @@ -122,15 +122,15 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { @@ -138,7 +138,7 @@ ColumnLayout { anchors.centerIn: parent text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: modelData.enabled && VPN.connected ? 1 : 0 color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } @@ -159,7 +159,7 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -196,8 +196,7 @@ ColumnLayout { return Colours.palette.m3tertiary; return Colours.palette.m3onSurface; } - font.pointSize: Tokens.font.size.small - font.weight: modelData.enabled && VPN.connected ? 500 : 400 + font: Tokens.font.body.builders.small.weight(modelData.enabled && VPN.connected ? Font.Medium : Font.Normal).build() elide: Text.ElideRight } } @@ -205,7 +204,7 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) @@ -265,7 +264,7 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: deleteIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: deleteIcon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: "transparent" @@ -363,7 +362,7 @@ ColumnLayout { parent: Overlay.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) - implicitWidth: Math.min(400, parent.width - Tokens.padding.large * 2) + implicitWidth: Math.min(400, parent.width - Tokens.padding.extraLargeIncreased) padding: Tokens.padding.large * 1.5 modal: true @@ -416,7 +415,7 @@ ColumnLayout { background: StyledRect { color: Colours.palette.m3surfaceContainerHigh - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge Elevation { anchors.fill: parent @@ -445,7 +444,7 @@ ColumnLayout { id: selectionContent anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium visible: vpnDialog.currentState === "selection" opacity: vpnDialog.currentState === "selection" ? 1 : 0 @@ -457,8 +456,7 @@ ColumnLayout { StyledText { text: qsTr("Add VPN Provider") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { @@ -466,11 +464,11 @@ ColumnLayout { text: qsTr("Choose a provider to add") wrapMode: Text.WordWrap color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } TextButton { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true text: qsTr("NetBird") inactiveColour: Colours.tPalette.m3surfaceContainerHigh @@ -551,7 +549,7 @@ ColumnLayout { } TextButton { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.palette.m3secondaryContainer @@ -564,7 +562,7 @@ ColumnLayout { id: formContent anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium visible: vpnDialog.currentState === "form" opacity: vpnDialog.currentState === "form" ? 1 : 0 @@ -576,17 +574,16 @@ ColumnLayout { StyledText { text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 StyledText { text: qsTr("Display Name") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -594,7 +591,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -609,7 +606,7 @@ ColumnLayout { id: displayNameField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: vpnDialog.displayName onTextChanged: vpnDialog.displayName = text @@ -619,11 +616,11 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -631,7 +628,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -646,7 +643,7 @@ ColumnLayout { id: interfaceNameField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: vpnDialog.interfaceName onTextChanged: vpnDialog.interfaceName = text @@ -656,12 +653,12 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") StyledText { text: qsTr("Connect Command (e.g., wg-quick up wg0)") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -669,7 +666,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -684,7 +681,7 @@ ColumnLayout { id: connectCmdField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: vpnDialog.connectCmd onTextChanged: vpnDialog.connectCmd = text @@ -694,12 +691,12 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller / 2 + spacing: Tokens.spacing.medium / 2 visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") StyledText { text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } @@ -707,7 +704,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Tokens.rounding.small + radius: Tokens.rounding.medium border.width: 1 border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -722,7 +719,7 @@ ColumnLayout { id: disconnectCmdField anchors.centerIn: parent - width: parent.width - Tokens.padding.normal + width: parent.width - Tokens.padding.medium horizontalAlignment: TextInput.AlignLeft text: vpnDialog.disconnectCmd onTextChanged: vpnDialog.disconnectCmd = text @@ -731,9 +728,9 @@ ColumnLayout { } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { Layout.fillWidth: true diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 064f32a1..328292e5 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -18,7 +18,7 @@ ColumnLayout { required property Session session - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "vpn_key" @@ -26,7 +26,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("General") description: qsTr("VPN configuration") } @@ -42,20 +42,20 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Providers") description: qsTr("Manage VPN providers") } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium ListView { Layout.fillWidth: true Layout.preferredHeight: contentHeight interactive: false - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium model: ScriptModel { values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { @@ -83,18 +83,18 @@ ColumnLayout { width: ListView.view ? ListView.view.width : undefined implicitHeight: 60 color: Colours.tPalette.m3surfaceContainerHigh - radius: Tokens.rounding.normal + radius: Tokens.rounding.large RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.normal + anchors.margins: Tokens.padding.medium + spacing: Tokens.spacing.medium MaterialIcon { text: modelData.isActive ? "vpn_key" : "vpn_key_off" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline } @@ -104,12 +104,12 @@ ColumnLayout { StyledText { text: modelData.displayName - font.weight: modelData.isActive ? 500 : 400 + font: Tokens.font.body.builders.medium.weight(modelData.isActive ? Font.Medium : Font.Normal).build() } StyledText { text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline } } @@ -183,7 +183,7 @@ ColumnLayout { TextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium text: qsTr("+ Add Provider") inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer @@ -195,13 +195,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Quick Add") description: qsTr("Add common VPN providers") } SectionContainer { - contentSpacing: Tokens.spacing.smaller + contentSpacing: Tokens.spacing.medium TextButton { Layout.fillWidth: true diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index e39744ca..51123a29 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -64,7 +64,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Connection status") @@ -86,8 +86,8 @@ DeviceDetails { TextButton { Layout.fillWidth: true - Layout.topMargin: Tokens.spacing.normal - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.topMargin: Tokens.spacing.medium + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 visible: { if (!root.network || !root.network.ssid) { return false; @@ -112,7 +112,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Network properties") @@ -120,7 +120,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("SSID") @@ -155,7 +155,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionHeader { title: qsTr("Connection information") diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index b6acd079..6dc29d69 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -34,7 +34,7 @@ DeviceList { visible: Nmcli.scanning text: qsTr("Scanning...") color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.small + font: Tokens.font.title.small } } @@ -48,12 +48,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Settings") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } Item { @@ -64,9 +63,9 @@ DeviceList { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small onClicked: { Nmcli.toggleWifi(null); @@ -77,9 +76,9 @@ DeviceList { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small onClicked: { Nmcli.rescanWifi(); @@ -90,9 +89,9 @@ DeviceList { toggled: !root.session.network.active icon: "settings" accent: "Primary" - iconSize: Tokens.font.size.normal - horizontalPadding: Tokens.padding.normal - verticalPadding: Tokens.padding.smaller + iconSize: Tokens.font.body.medium.pointSize + horizontalPadding: Tokens.padding.medium + verticalPadding: Tokens.padding.small onClicked: { if (root.session.network.active) @@ -110,10 +109,10 @@ DeviceList { required property var modelData width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.medium * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Tokens.rounding.normal + radius: Tokens.rounding.large StateLayer { onClicked: { @@ -130,15 +129,15 @@ DeviceList { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { @@ -146,7 +145,7 @@ DeviceList { anchors.centerIn: parent text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large fill: modelData.active ? 1 : 0 color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } @@ -167,7 +166,7 @@ DeviceList { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -182,8 +181,7 @@ DeviceList { return qsTr("Open"); } color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - font.weight: modelData.active ? 500 : 400 + font: Tokens.font.body.builders.small.weight(modelData.active ? Font.Medium : Font.Normal).build() elide: Text.ElideRight } } @@ -191,7 +189,7 @@ DeviceList { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index ac88f9fc..4e793a53 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -99,9 +99,9 @@ Item { anchors.centerIn: parent implicitWidth: 400 - implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + implicitHeight: content.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 @@ -144,26 +144,25 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledText { @@ -182,10 +181,9 @@ Item { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - font.weight: 400 + font: Tokens.font.body.builders.small.weight(Font.Normal).build() wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Tokens.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.extraLargeIncreased } Item { @@ -193,9 +191,9 @@ Item { property string passwordBuffer: "" - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.medium * 2) focus: true Keys.onPressed: event => { if (!activeFocus) { @@ -261,7 +259,7 @@ Item { StyledRect { anchors.fill: parent - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) border.color: { @@ -302,8 +300,7 @@ Item { anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { @@ -318,10 +315,10 @@ Item { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Tokens.font.size.normal + implicitHeight: Tokens.font.body.medium.pointSize orientation: Qt.Horizontal - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall interactive: false model: ScriptModel { @@ -335,7 +332,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Tokens.rounding.small / 2 + radius: Tokens.rounding.medium / 2 opacity: 0 scale: 0 @@ -390,15 +387,15 @@ Item { } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -413,7 +410,7 @@ Item { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + Layout.minimumHeight: Tokens.font.body.medium.pointSize + Tokens.padding.medium * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index f3f1fd4b..bf55497a 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -15,7 +15,7 @@ ColumnLayout { required property Session session - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SettingsHeader { icon: "wifi" @@ -23,7 +23,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("WiFi status") description: qsTr("General WiFi settings") } @@ -39,13 +39,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Tokens.spacing.large + Layout.topMargin: Tokens.spacing.largeIncreased title: qsTr("Network information") description: qsTr("Current network connection") } SectionContainer { - contentSpacing: Tokens.spacing.small / 2 + contentSpacing: Tokens.spacing.extraSmall PropertyRow { label: qsTr("Connected network") diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index eda4e213..90ccf25d 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -64,9 +64,9 @@ Item { id: notificationsClippingRect anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.medium color: "transparent" radius: notificationsBorder.innerRadius @@ -75,7 +75,7 @@ Item { id: notificationsLoader anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.margins: Tokens.padding.large + Tokens.padding.medium anchors.leftMargin: Tokens.padding.large anchors.rightMargin: Tokens.padding.large @@ -87,7 +87,7 @@ Item { id: notificationsBorder leftThickness: 0 - rightThickness: Tokens.padding.normal + rightThickness: Tokens.padding.medium } Component { @@ -109,13 +109,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium ColumnLayout { Layout.fillWidth: true Layout.maximumWidth: 500 Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionContainer { Layout.fillWidth: true @@ -123,7 +123,7 @@ Item { StyledText { text: qsTr("Notifications") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SplitButtonRow { @@ -218,7 +218,7 @@ Item { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionContainer { Layout.fillWidth: true @@ -226,7 +226,7 @@ Item { StyledText { text: qsTr("Toast settings") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SplitButtonRow { @@ -314,8 +314,8 @@ Item { GridLayout { Layout.fillWidth: true columns: 2 - columnSpacing: Tokens.spacing.normal - rowSpacing: Tokens.spacing.normal + columnSpacing: Tokens.spacing.medium + rowSpacing: Tokens.spacing.medium SwitchRow { Layout.fillWidth: true diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 601fc09f..f4ed385d 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -125,9 +125,9 @@ Item { id: taskbarClippingRect anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.medium radius: taskbarBorder.innerRadius color: "transparent" @@ -136,7 +136,7 @@ Item { id: taskbarLoader anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.margins: Tokens.padding.large + Tokens.padding.medium anchors.leftMargin: Tokens.padding.large anchors.rightMargin: Tokens.padding.large @@ -149,7 +149,7 @@ Item { id: taskbarBorder leftThickness: 0 - rightThickness: Tokens.padding.normal + rightThickness: Tokens.padding.medium } Component { @@ -172,15 +172,14 @@ Item { anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { text: qsTr("Taskbar") - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.title.builders.medium.weight(Font.Medium).build() } } @@ -190,7 +189,7 @@ Item { StyledText { text: qsTr("Status Icons") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } ConnectedButtonGroup { @@ -269,14 +268,14 @@ Item { id: mainRowLayout Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium ColumnLayout { id: leftColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionContainer { Layout.fillWidth: true @@ -284,13 +283,13 @@ Item { StyledText { text: qsTr("Workspaces") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } StyledRect { Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: workspacesShownRow.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -304,7 +303,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -325,8 +324,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -340,7 +339,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -359,8 +358,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: workspacesOccupiedBgRow.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -374,7 +373,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -393,8 +392,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: workspacesShowWindowsRow.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -408,7 +407,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -427,8 +426,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -442,7 +441,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -463,8 +462,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal + implicitHeight: workspacesPerMonitorRow.implicitHeight + Tokens.padding.extraLargeIncreased + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -478,7 +477,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -502,7 +501,7 @@ Item { StyledText { text: qsTr("Scroll Actions") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } ConnectedButtonGroup { @@ -543,7 +542,7 @@ Item { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionContainer { Layout.fillWidth: true @@ -551,7 +550,7 @@ Item { StyledText { text: qsTr("Clock") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SwitchRow { @@ -588,7 +587,7 @@ Item { StyledText { text: qsTr("Bar Behavior") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SwitchRow { @@ -610,7 +609,7 @@ Item { } SectionContainer { - contentSpacing: Tokens.spacing.normal + contentSpacing: Tokens.spacing.medium SliderInput { Layout.fillWidth: true @@ -641,7 +640,7 @@ Item { StyledText { text: qsTr("Active window") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SwitchRow { @@ -669,7 +668,7 @@ Item { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium SectionContainer { Layout.fillWidth: true @@ -677,7 +676,7 @@ Item { StyledText { text: qsTr("Popouts") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } SwitchRow { @@ -714,7 +713,7 @@ Item { StyledText { text: qsTr("Tray Settings") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } ConnectedButtonGroup { @@ -755,7 +754,7 @@ Item { StyledText { text: qsTr("Monitors") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small } ConnectedButtonGroup { diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 95d2a08a..0a1e23c2 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia import Caelestia.Config import qs.components import qs.components.filedialog @@ -12,15 +13,6 @@ Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i) as Loader; - if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) - return true; - } - return false; - } required property DashboardState dashState required property FileDialog facePicker @@ -42,7 +34,7 @@ Item { component: performanceComponent, iconName: "speed", text: qsTr("Performance"), - enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery) + enabled: Config.dashboard.showPerformance }, { component: weatherComponent, @@ -66,7 +58,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Tokens.padding.normal + anchors.topMargin: CUtils.clamp(anchors.margins - Config.border.thickness, 0, anchors.margins) anchors.margins: Tokens.padding.large nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 @@ -83,7 +75,7 @@ Item { anchors.bottom: parent.bottom anchors.margins: Tokens.padding.large - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: "transparent" Flickable { @@ -174,7 +166,7 @@ Item { Component { id: mediaComponent - MediaWrapper { + Media { visibilities: root.visibilities } } @@ -198,14 +190,10 @@ Item { } Behavior on implicitWidth { - Anim { - type: Anim.EmphasizedLarge - } + Anim {} } Behavior on implicitHeight { - Anim { - type: Anim.EmphasizedLarge - } + Anim {} } } diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 6785ede8..02e16231 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -12,16 +12,16 @@ GridLayout { required property DashboardState dashState required property FileDialog facePicker - rowSpacing: Tokens.spacing.normal - columnSpacing: Tokens.spacing.normal + rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.medium Rect { Layout.column: 2 Layout.columnSpan: 3 - Layout.preferredWidth: user.implicitWidth - Layout.preferredHeight: user.implicitHeight + Layout.preferredWidth: Tokens.sizes.dashboard.userWidth + Layout.fillHeight: true - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge User { id: user @@ -35,11 +35,13 @@ GridLayout { Layout.row: 0 Layout.columnSpan: 2 Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth - Layout.fillHeight: true + Layout.preferredHeight: weather.implicitHeight - radius: Tokens.rounding.large * 1.5 + radius: Tokens.rounding.extraLarge * 1.5 - SmallWeather {} + SmallWeather { + id: weather + } } Rect { @@ -47,7 +49,7 @@ GridLayout { Layout.preferredWidth: dateTime.implicitWidth Layout.fillHeight: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large DateTime { id: dateTime @@ -61,7 +63,7 @@ GridLayout { Layout.fillWidth: true Layout.preferredHeight: calendar.implicitHeight - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge Calendar { id: calendar @@ -76,7 +78,7 @@ GridLayout { Layout.preferredWidth: resources.implicitWidth Layout.fillHeight: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Resources { id: resources @@ -90,7 +92,7 @@ GridLayout { Layout.preferredWidth: media.implicitWidth Layout.fillHeight: true - radius: Tokens.rounding.large * 2 + radius: Tokens.rounding.extraLarge * 2 Media { id: media diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml deleted file mode 100644 index 1e5461f5..00000000 --- a/modules/dashboard/LyricMenu.qml +++ /dev/null @@ -1,412 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -StyledRect { - id: root - - required property real contentHeight - - function searchCandidates(title, artist) { - LyricsService.currentRequestId++; - LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); - } - - implicitHeight: contentHeight - - radius: Tokens.rounding.large - color: Colours.tPalette.m3surfaceContainer - - Loader { - asynchronous: true - anchors.fill: parent - active: root.height > 0 - - sourceComponent: ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - // Header: icon, backend selector, refresh, toggle - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - MaterialIcon { - text: "lyrics" - fill: 1 - color: Colours.palette.m3primary - font.pointSize: Tokens.spacing.large - } - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: 80 - radius: Tokens.rounding.small - color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) - - StyledText { - anchors.centerIn: parent - text: LyricsService.preferredBackend - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3primary - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - const backends = ["Auto", "Local", "NetEase"]; - const currentIndex = backends.indexOf(LyricsService.preferredBackend); - const nextIndex = (currentIndex + 1) % backends.length; - LyricsService.preferredBackend = backends[nextIndex]; - LyricsService.loadLyrics(); - } - } - } - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: 60 - radius: Tokens.rounding.small - visible: LyricsService.preferredBackend === "Auto" - color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) - - StyledText { - anchors.centerIn: parent - text: LyricsService.backend - font.pointSize: Tokens.font.size.small - color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary - } - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: "refresh" - type: IconButton.Text - onClicked: LyricsService.loadLyrics() - } - - StyledSwitch { - checked: LyricsService.lyricsVisible - onToggled: LyricsService.toggleVisibility() - } - } - - StyledText { - Layout.fillWidth: true - text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 - } - - // Local file info (shown in Local mode) - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 48 - visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 - radius: Tokens.rounding.small - color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.small - spacing: 0 - - StyledText { - Layout.fillWidth: true - text: { - const path = LyricsService.loadedLocalFile; - const parts = path.split('/'); - return parts[parts.length - 1]; - } - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3tertiary - elide: Text.ElideMiddle - } - - StyledText { - Layout.fillWidth: true - text: { - const path = LyricsService.loadedLocalFile; - const parts = path.split('/'); - if (parts.length > 2) { - return parts.slice(-3, -1).join('/'); - } - return ""; - } - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - elide: Text.ElideMiddle - } - } - } - - // Candidates list - Loader { - Layout.fillWidth: true - Layout.fillHeight: true - - active: LyricsService.preferredBackend !== "Local" - - sourceComponent: ListView { - id: candidatesView - - model: LyricsService.candidatesModel - clip: true - spacing: Tokens.spacing.small - visible: LyricsService.candidatesModel.count > 0 - opacity: visible ? 1 : 0 - - delegate: Item { - id: delegateRoot - - required property real id - required property string title - required property string artist - - property bool hovered: false - property bool pressed: false - - width: ListView.view.width * 0.98 - height: 70 - - anchors.horizontalCenter: parent?.horizontalCenter - scale: hovered ? 1.02 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: Tokens.anim.durations.small - easing.type: Easing.OutCubic - } - } - - Rectangle { - id: background - - anchors.fill: parent - radius: Tokens.rounding.small - - color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) - - border.width: delegateRoot.hovered ? 1 : 0 - border.color: Colours.palette.m3primary - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - Behavior on border.width { - NumberAnimation { - duration: Tokens.anim.durations.small - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onEntered: delegateRoot.hovered = true - onExited: delegateRoot.hovered = false - onPressed: delegateRoot.pressed = true - onReleased: delegateRoot.pressed = false - onClicked: LyricsService.selectCandidate(delegateRoot.id) - } - - Row { - anchors.fill: parent - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.small - - // Active indicator bar - Rectangle { - width: 4 - height: parent.height * 0.6 - radius: 2 - anchors.verticalCenter: parent.verticalCenter - color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 30 - spacing: 4 - - Text { - text: delegateRoot.title - font.pointSize: Tokens.font.size.normal - font.bold: true - color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface - width: parent.width - elide: Text.ElideRight - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - } - - Text { - text: delegateRoot.artist - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight - } - } - } - } - } - } - - Item { - Layout.fillHeight: true - visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" - } - - // Manual search - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - StyledText { - Layout.fillWidth: true - text: "Manual Search" - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - StyledInputField { - id: searchTitle - - Layout.fillWidth: true - horizontalAlignment: TextInput.AlignLeft - - Binding { - target: searchTitle - property: "text" - value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") - } - } - - StyledInputField { - id: searchArtist - - Layout.fillWidth: true - horizontalAlignment: TextInput.AlignLeft - - Binding { - target: searchArtist - property: "text" - value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") - } - } - - IconButton { - icon: "search" - onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) - } - } - } - - // Offset controls - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.small - - MaterialIcon { - text: "contrast_square" - font.pointSize: Tokens.font.size.large - color: Colours.palette.m3secondary - } - - StyledText { - text: "Offset" - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: "remove" - type: IconButton.Text - onClicked: { - LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); - LyricsService.savePrefs(); - } - } - - TextInput { - id: offsetInput - - horizontalAlignment: TextInput.AlignHCenter - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.normal - selectByMouse: true - text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" - onEditingFinished: { - let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); - let val = parseFloat(cleaned); - if (!isNaN(val)) { - LyricsService.offset = parseFloat(val.toFixed(1)); - LyricsService.savePrefs(); - } else { - offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; - } - } - - Binding { - target: offsetInput - property: "text" - value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" - when: !offsetInput.activeFocus - } - - Connections { - function onCurrentRequestIdChanged() { - offsetInput.focus = false; - } - - target: LyricsService - } - } - - IconButton { - icon: "add" - type: IconButton.Text - onClicked: { - LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); - LyricsService.savePrefs(); - } - } - } - } - } -} diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml deleted file mode 100644 index c0188304..00000000 --- a/modules/dashboard/LyricsView.qml +++ /dev/null @@ -1,112 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.services - -StyledListView { - id: root - - readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 - - clip: true - model: LyricsService.model - currentIndex: LyricsService.currentIndex - visible: lyricsActuallyVisible || hideTimer.running - preferredHighlightBegin: height / 2 - 30 - preferredHighlightEnd: height / 2 + 30 - highlightRangeMode: ListView.ApplyRange - highlightFollowsCurrentItem: true - highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal - layer.enabled: true - layer.effect: ShaderEffect { - required property Item source - property real fadeMargin: 0.5 - - fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") - } - onLyricsActuallyVisibleChanged: { - if (!lyricsActuallyVisible) - hideTimer.restart(); - } - onModelChanged: { - if (model && model.count > 0) { - Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); - } - } - delegate: Item { - id: delegateRoot - - required property string lyricLine - required property real time - required property int index - readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 - property bool isCurrent: ListView.isCurrentItem - - width: ListView.view.width - height: hasContent ? (lyricText.contentHeight + Tokens.spacing.large) : 0 - - MultiEffect { - id: effect - - anchors.fill: lyricText - source: lyricText - scale: lyricText.scale - enabled: delegateRoot.isCurrent - visible: delegateRoot.isCurrent - - blurEnabled: true - blur: 0.4 - - shadowEnabled: true - shadowColor: Colours.palette.m3primary - shadowOpacity: 0.5 - shadowBlur: 0.6 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 0 - - autoPaddingEnabled: true - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) - } - - Text { - id: lyricText - - text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" - width: parent.width * 0.85 - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - font.pointSize: Tokens.font.size.normal - color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - font.bold: delegateRoot.isCurrent - scale: delegateRoot.isCurrent ? 1.15 : 1.0 - - Behavior on color { - CAnim { - duration: Tokens.anim.durations.small - } - } - Behavior on scale { - Anim { - type: Anim.StandardSmall - } - } - } - } - - Timer { - id: hideTimer - - interval: 300 // long enough to bridge the track switch gap - running: false - repeat: false - } -} diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 367d9e98..2a2f5c2e 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,513 +1,150 @@ -pragma ComponentBehavior: Bound - +import "media" import QtQuick import QtQuick.Layouts -import QtQuick.Shapes -import Quickshell -import Quickshell.Services.Mpris +import M3Shapes import Caelestia.Config -import Caelestia.Services import qs.components -import qs.components.controls import qs.services -import qs.utils Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: lyricMenuOpen - readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 - readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight + implicitWidth: Tokens.sizes.dashboard.mediaTabWidth + implicitHeight: Tokens.sizes.dashboard.mediaTabHeight - property bool lyricMenuOpen: false - property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 - property bool lyricsShowingDebounced: false - - property real playerProgress: { - const active = Players.active; - return active?.length ? (active.position % active.length) / active.length : 0; - } - - function lengthStr(length: int): string { - if (length < 0) - return "-1:-1"; - - const hours = Math.floor(length / 3600); - const mins = Math.floor((length % 3600) / 60); - const secs = Math.floor(length % 60).toString().padStart(2, "0"); - - if (hours > 0) - return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; - return `${mins}:${secs}`; - } - - onLyricsShowingChanged: { - if (lyricsShowing) { - lyricsHideDelay.stop(); - lyricsShowingDebounced = true; - } else { - lyricsHideDelay.restart(); - } - } - - implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 - implicitHeight: nonAnimHeight - - Behavior on implicitHeight { - Anim {} - } - - Behavior on playerProgress { - Anim { - type: Anim.StandardLarge - } - } - - Timer { - running: Players.active?.isPlaying ?? false - interval: GlobalConfig.dashboard.mediaUpdateInterval - triggeredOnStart: true - repeat: true - onTriggered: { - if (!Players.active) - return; - LyricsService.updatePosition(); - Players.active?.positionChanged(); - } - } - - Timer { - id: lyricsHideDelay - - interval: 300 - repeat: false - } - - Connections { - function onTriggered() { - root.lyricsShowingDebounced = false; - } - - target: lyricsHideDelay - } - - ServiceRef { - service: Audio.cava - } - - ServiceRef { - service: Audio.beatTracker - } - - Shape { - id: visualiser - - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 - readonly property real innerX: cover.implicitWidth / 2 + Tokens.spacing.small - readonly property real innerY: cover.implicitHeight / 2 + Tokens.spacing.small - property color colour: Colours.palette.m3primary - - anchors.fill: cover - anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize - - asynchronous: true - preferredRendererType: Shape.CurveRenderer - data: visualiserBars.instances - } - - Variants { - id: visualiserBars - - model: Array.from({ - length: GlobalConfig.services.visualiserBars - }, (_, i) => i) - - ShapePath { - id: visualiserBar - - required property int modelData - readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) - - readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars - readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize - readonly property real cos: Math.cos(angle) - readonly property real sin: Math.sin(angle) - - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 - strokeColor: Colours.palette.m3primary - - startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos - startY: visualiser.centerY + (visualiser.innerY + strokeWidth / 2) * sin - - PathLine { - x: visualiser.centerX + (visualiser.innerX + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos - y: visualiser.centerY + (visualiser.innerY + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin - } - - Behavior on strokeColor { - CAnim {} - } - } - } - - StyledClippingRect { - id: cover - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize - - implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize - implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize - - color: Colours.tPalette.m3surfaceContainerHigh - radius: Infinity - - MaterialIcon { - anchors.centerIn: parent - - grade: 200 - text: "art_track" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: (parent.width * 0.4) || 1 - } - - Image { - id: image - - anchors.fill: parent - - source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize: { - const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; - return Qt.size(width * dpr, height * dpr); - } - - MouseArea { - anchors.fill: parent - onClicked: { - LyricsService.toggleVisibility(); - } - } - } - } - - ColumnLayout { - id: details - - anchors.verticalCenter: parent.verticalCenter - anchors.left: visualiser.right - anchors.leftMargin: Tokens.spacing.normal - - spacing: Tokens.spacing.small - - StyledText { - id: title - - Layout.fillWidth: true - Layout.maximumWidth: parent.implicitWidth - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") - color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - - StyledText { - id: album - - Layout.fillWidth: true - Layout.maximumWidth: parent.implicitWidth - - animate: true - horizontalAlignment: Text.AlignHCenter - visible: !!Players.active - text: Players.active?.trackAlbum || qsTr("Unknown album") - color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - - StyledText { - id: artist - - Layout.fillWidth: true - Layout.maximumWidth: parent.implicitWidth - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackArtist ?? qsTr("Play some music for stuff to show up here!")) || qsTr("Unknown artist") - color: Players.active ? Colours.palette.m3secondary : Colours.palette.m3outline - elide: Text.ElideRight - wrapMode: Players.active ? Text.NoWrap : Text.WordWrap - } - - LyricsView { - id: lyricsViewInDetails - - Layout.fillWidth: true - Layout.preferredHeight: 200 - } - - RowLayout { - id: controls - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.spacing.small - Layout.bottomMargin: Tokens.spacing.smaller - - spacing: Tokens.spacing.small - - PlayerControl { - type: IconButton.Text - icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" - font.pointSize: Math.round(Tokens.font.size.large) - disabled: !Players.active?.shuffleSupported - onClicked: Players.active.shuffle = !Players.active?.shuffle - } - - PlayerControl { - type: IconButton.Text - icon: "skip_previous" - font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canGoPrevious - onClicked: Players.active?.previous() - } - - PlayerControl { - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - label.animate: true - toggle: true - padding: Tokens.padding.small / 2 - checked: Players.active?.isPlaying ?? false - font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canTogglePlaying - onClicked: Players.active?.togglePlaying() - } - - PlayerControl { - type: IconButton.Text - icon: "skip_next" - font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canGoNext - onClicked: Players.active?.next() - } - - PlayerControl { - type: IconButton.Text - icon: "lyrics" - font.pointSize: Math.round(Tokens.font.size.large) - onClicked: root.lyricMenuOpen = !root.lyricMenuOpen - } - } - - StyledSlider { - id: slider - - enabled: !!Players.active - implicitWidth: 280 - implicitHeight: Tokens.padding.normal * 3 - - onMoved: { - const active = Players.active; - if (active?.canSeek && active?.positionSupported) - active.position = value * active.length; - } - - Binding { - target: slider - property: "value" - value: root.playerProgress - when: !slider.pressed - } - - CustomMouseArea { - function onWheel(event: WheelEvent) { - const active = Players.active; - if (!active?.canSeek || !active?.positionSupported) - return; - - event.accepted = true; - const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds - Qt.callLater(() => { - active.position = Math.max(0, Math.min(active.length, active.position + delta)); - }); - } - - anchors.fill: parent - acceptedButtons: Qt.NoButton - } - } - - Item { - Layout.fillWidth: true - implicitHeight: Math.max(position.implicitHeight, length.implicitHeight) - - StyledText { - id: position - - anchors.left: parent.left - - text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - } - - StyledText { - id: length - - anchors.right: parent.right - - text: root.lengthStr(Players.active?.length ?? -1) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - } - } - } - - ColumnLayout { - id: leftSection - - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 - anchors.left: details.right - anchors.leftMargin: Tokens.spacing.normal - - visible: lyricMenu.height === 0 || opacity > 0 - opacity: lyricMenu.height === 0 ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: Tokens.anim.durations.normal - easing.type: Easing.OutCubic - } - } - - Item { - id: bongocat - - implicitWidth: visualiser.width - implicitHeight: visualiser.height - - AnimatedImage { - anchors.centerIn: parent - - width: visualiser.width * 0.75 - height: visualiser.height * 0.75 - - playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit - } - } - } - - LyricMenu { - id: lyricMenu - - anchors.top: parent.top - anchors.left: details.right - anchors.right: parent.right - anchors.leftMargin: Tokens.spacing.normal - - contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight - - visible: root.lyricMenuOpen || height > 0 - height: root.lyricMenuOpen ? implicitHeight : 0 - clip: true - - Behavior on height { - NumberAnimation { - duration: Tokens.anim.durations.normal - easing.type: Easing.OutCubic - } - } + BackgroundShapes { + anchors.fill: parent } RowLayout { - id: playerChanger + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.extraLarge - parent: !root.lyricsShowingDebounced ? details : leftSection - Layout.alignment: Qt.AlignHCenter - spacing: Tokens.spacing.small + CoverVisualiser { + Layout.fillHeight: true + implicitWidth: Tokens.sizes.dashboard.mediaSectionWidth + } - PlayerControl { - type: IconButton.Text - icon: "move_up" - inactiveOnColour: Colours.palette.m3secondary - padding: Tokens.padding.small - font.pointSize: Tokens.font.size.large - disabled: !Players.active?.canRaise - onClicked: { - Players.active?.raise(); - root.visibilities.dashboard = false; + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + state: Players.active ? "" : "noMedia" + + states: State { + name: "noMedia" + + PropertyChanges { + noMedia.opacity: 1 + content.opacity: 0 + } } - } - SplitButton { - id: playerSelector + transitions: [ + Transition { + from: "" - disabled: !Players.list.length - active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData + SequentialAnimation { + Anim { + target: content + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + target: noMedia + property: "opacity" + type: Anim.SlowEffects + } + } + }, + Transition { + to: "" - menuItems: playerList.instances - fallbackIcon: "music_off" - fallbackText: qsTr("No players") + SequentialAnimation { + Anim { + target: noMedia + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + target: content + property: "opacity" + type: Anim.SlowEffects + } + } + } + ] - label.Layout.maximumWidth: slider.implicitWidth * 0.28 - label.elide: Text.ElideRight + Loader { + id: noMedia - stateLayer.disabled: true - menuOnTop: true + anchors.centerIn: parent + anchors.horizontalCenterOffset: -Tokens.padding.extraLarge * 2 + asynchronous: true + active: opacity > 0 + opacity: 0 - Variants { - id: playerList + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.small - model: Players.list + MaterialShape { + Layout.topMargin: (pathBounds().height - implicitSize) / 2 + Layout.bottomMargin: (pathBounds().height - implicitSize) / 2 + Tokens.spacing.small + Layout.alignment: Qt.AlignHCenter + color: Colours.palette.m3primaryContainer + implicitSize: icon.implicitHeight + Tokens.padding.extraLarge * 2 + shape: MaterialShape.ClamShell - PlayerItem {} + Behavior on color { + CAnim {} + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "queue_music" + fontStyle: Tokens.font.icon.builders.large.scale(2).build() + color: Colours.palette.m3onPrimaryContainer + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Nothing playing") + font: Tokens.font.headline.medium + } + + StyledText { + text: qsTr("Play something for it to show up here!") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + } } - } - PlayerControl { - type: IconButton.Text - icon: "delete" - inactiveOnColour: Colours.palette.m3error - padding: Tokens.padding.small - font.pointSize: Tokens.font.size.large - disabled: !Players.active?.canQuit - onClicked: Players.active?.quit() - } - } + Loader { + id: content - component PlayerItem: MenuItem { - required property MprisPlayer modelData + anchors.fill: parent + asynchronous: true + active: opacity > 0 - icon: modelData === Players.active ? "check" : "" - text: Players.getIdentity(modelData) - activeIcon: "animated_images" - } + sourceComponent: RowLayout { + spacing: Tokens.spacing.extraLarge - component PlayerControl: IconButton { - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) - radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 - radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial - radiusAnim.easing: Tokens.anim.expressiveFastSpatial + Details { + Layout.fillWidth: true + } - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial + LyricsAndSelector { + Layout.fillHeight: true + implicitWidth: Tokens.sizes.dashboard.mediaSectionWidth + } + } } } } diff --git a/modules/dashboard/MediaWrapper.qml b/modules/dashboard/MediaWrapper.qml deleted file mode 100644 index b03a11ee..00000000 --- a/modules/dashboard/MediaWrapper.qml +++ /dev/null @@ -1,13 +0,0 @@ -import QtQuick - -Item { - property alias visibilities: media.visibilities - readonly property alias needsKeyboard: media.needsKeyboard - - implicitWidth: media.implicitWidth - implicitHeight: media.nonAnimHeight - - Media { - id: media - } -} diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index c93499ca..3973bdc0 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -1,57 +1,47 @@ +import "performance" import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.UPower import Caelestia.Config -import Caelestia.Internal +import Caelestia.Services import qs.components -import qs.components.misc import qs.services Item { id: root - readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 + implicitWidth: placeholder.active ? Tokens.sizes.dashboard.perfPlaceholderWidth : content.implicitWidth + implicitHeight: placeholder.active ? placeholder.implicitHeight + Tokens.padding.extraLarge * 2 : content.implicitHeight - function displayTemp(temp: real): string { - return `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}`; - } - - implicitWidth: Math.max(minWidth, content.implicitWidth) - implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight - - StyledRect { + Loader { id: placeholder anchors.centerIn: parent - width: 400 - height: 350 - radius: Tokens.rounding.large - color: Colours.tPalette.m3surfaceContainer - visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) + active: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) + asynchronous: true - ColumnLayout { - anchors.centerIn: parent - spacing: Tokens.spacing.normal + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "tune" - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build() color: Colours.palette.m3onSurfaceVariant } StyledText { + Layout.topMargin: -Tokens.spacing.small Layout.alignment: Qt.AlignHCenter text: qsTr("No widgets enabled") - font.pointSize: Tokens.font.size.large + font: Tokens.font.title.large color: Colours.palette.m3onSurface } StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("Enable widgets in dashboard settings") - font.pointSize: Tokens.font.size.small + text: qsTr("Enable widgets in the dashboard settings") + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } } @@ -62,765 +52,95 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.normal - visible: !placeholder.visible - - Ref { - service: SystemUsage - } + spacing: Tokens.spacing.medium + visible: !placeholder.active ColumnLayout { id: mainColumn Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") + spacing: Tokens.spacing.medium + visible: cpuCard.active || gpuCard.active - HeroCard { - Layout.fillWidth: true - Layout.minimumWidth: 400 - Layout.preferredHeight: 150 - visible: Config.dashboard.performance.showCpu - icon: "memory" - title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") - mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` - mainLabel: qsTr("Usage") - secondaryValue: root.displayTemp(SystemUsage.cpuTemp) - secondaryLabel: qsTr("Temp") - usage: SystemUsage.cpuPerc - temperature: SystemUsage.cpuTemp - accentColor: Colours.palette.m3primary - } + WrappedLoader { + id: cpuCard - HeroCard { - Layout.fillWidth: true - Layout.minimumWidth: 400 - Layout.preferredHeight: 150 - visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" - icon: "desktop_windows" - title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") - mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` - mainLabel: qsTr("Usage") - secondaryValue: root.displayTemp(SystemUsage.gpuTemp) - secondaryLabel: qsTr("Temp") - usage: SystemUsage.gpuPerc - temperature: SystemUsage.gpuTemp - accentColor: Colours.palette.m3secondary - } - } + active: Config.dashboard.performance.showCpu - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork + sourceComponent: HeroCard { + icon: "memory" + label: qsTr("CPU") + subLabel: Cpu.name + usage: Cpu.percentage + temperature: Cpu.temperature + accent: Colours.palette.m3primary - GaugeCard { - Layout.minimumWidth: 250 - Layout.preferredHeight: 220 - Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork - icon: "memory_alt" - title: qsTr("Memory") - percentage: SystemUsage.memPerc - subtitle: { - const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); - const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); - return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; - } - accentColor: Colours.palette.m3tertiary - visible: Config.dashboard.performance.showMemory - } - - StorageGaugeCard { - Layout.minimumWidth: 250 - Layout.preferredHeight: 220 - Layout.fillWidth: !Config.dashboard.performance.showNetwork - visible: Config.dashboard.performance.showStorage - } - - NetworkCard { - Layout.fillWidth: true - Layout.minimumWidth: 200 - Layout.preferredHeight: 220 - visible: Config.dashboard.performance.showNetwork - } - } - } - - BatteryTank { - Layout.preferredWidth: 120 - Layout.preferredHeight: mainColumn.implicitHeight - visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery - } - } - - component BatteryTank: StyledClippingRect { - id: batteryTank - - property real percentage: UPower.displayDevice.percentage - property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging - property color accentColor: Colours.palette.m3primary - property real animatedPercentage: 0 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - Component.onCompleted: animatedPercentage = percentage - onPercentageChanged: animatedPercentage = percentage - - // Background Fill - StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: parent.height * batteryTank.animatedPercentage - color: Qt.alpha(batteryTank.accentColor, 0.15) - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small - - // Header Section - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.small - - MaterialIcon { - text: { - if (!UPower.displayDevice.isLaptopBattery) { - if (PowerProfiles.profile === PowerProfile.PowerSaver) - return "energy_savings_leaf"; - - if (PowerProfiles.profile === PowerProfile.Performance) - return "rocket_launch"; - - return "balance"; + ServiceRef { + service: Cpu } - if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) - return "battery_full"; - - const perc = UPower.displayDevice.percentage; - const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); - if (perc >= 0.99) - return "battery_full"; - - let level = Math.floor(perc * 7); - if (charging && (level === 4 || level === 1)) - level--; - - return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } - font.pointSize: Tokens.font.size.large - color: batteryTank.accentColor } - StyledText { - Layout.fillWidth: true - text: qsTr("Battery") - font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurface + WrappedLoader { + id: gpuCard + + active: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None + + sourceComponent: HeroCard { + icon: "desktop_windows" + label: qsTr("GPU") + subLabel: Gpu.name + usage: Gpu.percentage + temperature: Gpu.temperature + accent: Colours.palette.m3secondary + + ServiceRef { + service: Gpu + } + } } } - Item { - Layout.fillHeight: true - } + RowLayout { + spacing: Tokens.spacing.medium + visible: storageCard.active || networkCard.active || memoryCard.active - // Bottom Info Section - ColumnLayout { - Layout.fillWidth: true - spacing: -4 + WrappedLoader { + id: storageCard - StyledText { - Layout.alignment: Qt.AlignRight - text: `${Math.round(batteryTank.percentage * 100)}%` - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: batteryTank.accentColor + active: Config.dashboard.performance.showStorage + sourceComponent: StorageCard {} } - StyledText { - Layout.alignment: Qt.AlignRight - text: { - if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) - return qsTr("Full"); + WrappedLoader { + id: networkCard - if (batteryTank.isCharging) - return qsTr("Charging"); + active: Config.dashboard.performance.showNetwork + sourceComponent: NetworkCard {} + } - const s = UPower.displayDevice.timeToEmpty; - if (s === 0) - return qsTr("..."); + WrappedLoader { + id: memoryCard - const hr = Math.floor(s / 3600); - const min = Math.floor((s % 3600) / 60); - if (hr > 0) - return `${hr}h ${min}m`; - - return `${min}m`; - } - font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant + active: Config.dashboard.performance.showMemory + sourceComponent: MemoryCard {} } } } - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } + WrappedLoader { + Layout.fillWidth: false + active: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery + sourceComponent: BatteryTank {} } } - component CardHeader: RowLayout { - property string icon - property string title - property color accentColor: Colours.palette.m3primary - + component WrappedLoader: Loader { Layout.fillWidth: true - spacing: Tokens.spacing.small - - MaterialIcon { - text: parent.icon - fill: 1 - color: parent.accentColor - font.pointSize: Tokens.spacing.large - } - - StyledText { - Layout.fillWidth: true - text: parent.title - font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - } - - component ProgressBar: StyledRect { - id: progressBar - - property real value: 0 - property color fgColor: Colours.palette.m3primary - property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - property real animatedValue: 0 - - color: bgColor - radius: Tokens.rounding.full - Component.onCompleted: animatedValue = value - onValueChanged: animatedValue = value - - StyledRect { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width * progressBar.animatedValue - color: progressBar.fgColor - radius: Tokens.rounding.full - } - - Behavior on animatedValue { - Anim { - type: Anim.StandardLarge - } - } - } - - component HeroCard: StyledClippingRect { - id: heroCard - - property string icon - property string title - property string mainValue - property string mainLabel - property string secondaryValue - property string secondaryLabel - property real usage: 0 - property real temperature: 0 - property color accentColor: Colours.palette.m3primary - readonly property real maxTemp: 100 - readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) - property real animatedUsage: 0 - property real animatedTemp: 0 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - Component.onCompleted: { - animatedUsage = usage; - animatedTemp = tempProgress; - } - onUsageChanged: animatedUsage = usage - onTempProgressChanged: animatedTemp = tempProgress - - StyledRect { - anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - implicitWidth: parent.width * heroCard.animatedUsage - color: Qt.alpha(heroCard.accentColor, 0.15) - } - - CardHeader { - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: Tokens.padding.large - anchors.topMargin: Math.round(Tokens.padding.large * 1.2) - - width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Tokens.spacing.normal - icon: heroCard.icon - title: heroCard.title - accentColor: heroCard.accentColor - } - - Column { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: Math.round(Tokens.padding.large * 1.2) - anchors.bottomMargin: Math.round(Tokens.padding.large * 1.3) - - spacing: Tokens.spacing.small - - Row { - spacing: Tokens.spacing.small - - StyledText { - text: heroCard.secondaryValue - font.pointSize: Tokens.font.size.normal - font.weight: Font.Medium - } - - StyledText { - text: heroCard.secondaryLabel - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - anchors.baseline: parent.children[0].baseline - } - } - - ProgressBar { - implicitWidth: parent.width * 0.5 - implicitHeight: 6 - value: heroCard.tempProgress - fgColor: heroCard.accentColor - bgColor: Qt.alpha(heroCard.accentColor, 0.2) - } - } - - Column { - id: usageColumn - - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - anchors.rightMargin: 32 - spacing: 0 - - StyledText { - id: usageLabel - - anchors.right: parent.right - text: heroCard.mainLabel - font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - - StyledText { - anchors.right: parent.right - text: heroCard.mainValue - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: heroCard.accentColor - } - } - - Behavior on animatedUsage { - Anim { - type: Anim.StandardLarge - } - } - - Behavior on animatedTemp { - Anim { - type: Anim.StandardLarge - } - } - } - - component GaugeCard: StyledRect { - id: gaugeCard - - property string icon - property string title - property real percentage: 0 - property string subtitle - property color accentColor: Colours.palette.m3primary - readonly property real arcStartAngle: 0.75 * Math.PI - readonly property real arcSweep: 1.5 * Math.PI - property real animatedPercentage: 0 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - clip: true - Component.onCompleted: animatedPercentage = percentage - onPercentageChanged: animatedPercentage = percentage - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.smaller - - CardHeader { - icon: gaugeCard.icon - title: gaugeCard.title - accentColor: gaugeCard.accentColor - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ArcGauge { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - height: width - percentage: gaugeCard.animatedPercentage - accentColor: gaugeCard.accentColor - trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - startAngle: gaugeCard.arcStartAngle - sweepAngle: gaugeCard.arcSweep - } - - StyledText { - anchors.centerIn: parent - text: `${Math.round(gaugeCard.percentage * 100)}%` - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: gaugeCard.accentColor - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: gaugeCard.subtitle - font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant - } - } - - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } - } - } - - component StorageGaugeCard: StyledRect { - id: storageGaugeCard - - property int currentDiskIndex: 0 - readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null - property int diskCount: 0 - readonly property real arcStartAngle: 0.75 * Math.PI - readonly property real arcSweep: 1.5 * Math.PI - property real animatedPercentage: 0 - property color accentColor: Colours.palette.m3secondary - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - clip: true - Component.onCompleted: { - diskCount = SystemUsage.disks.length; - if (currentDisk) - animatedPercentage = currentDisk.perc; - } - onCurrentDiskChanged: { - if (currentDisk) - animatedPercentage = currentDisk.perc; - } - - // Update diskCount and animatedPercentage when disks data changes - Connections { - function onDisksChanged() { - if (SystemUsage.disks.length !== storageGaugeCard.diskCount) - storageGaugeCard.diskCount = SystemUsage.disks.length; - - // Update animated percentage when disk data refreshes - if (storageGaugeCard.currentDisk) - storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; - } - - target: SystemUsage - } - - MouseArea { - anchors.fill: parent - onWheel: wheel => { - if (wheel.angleDelta.y > 0) - storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; - else if (wheel.angleDelta.y < 0) - storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.smaller - - CardHeader { - icon: "hard_disk" - title: { - const base = qsTr("Storage"); - if (!storageGaugeCard.currentDisk) - return base; - - return `${base} - ${storageGaugeCard.currentDisk.mount}`; - } - accentColor: storageGaugeCard.accentColor - - // Scroll hint icon - MaterialIcon { - text: "unfold_more" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal - visible: storageGaugeCard.diskCount > 1 - opacity: 0.7 - ToolTip.visible: hintHover.hovered - ToolTip.text: qsTr("Scroll to switch disks") - ToolTip.delay: 500 - - HoverHandler { - id: hintHover - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ArcGauge { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - height: width - percentage: storageGaugeCard.animatedPercentage - accentColor: storageGaugeCard.accentColor - trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - startAngle: storageGaugeCard.arcStartAngle - sweepAngle: storageGaugeCard.arcSweep - } - - StyledText { - anchors.centerIn: parent - text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" - font.pointSize: Tokens.font.size.extraLarge - font.weight: Font.Medium - color: storageGaugeCard.accentColor - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: { - if (!storageGaugeCard.currentDisk) - return "—"; - - const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); - const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); - return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; - } - font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant - } - } - - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } - } - } - - component NetworkCard: StyledRect { - id: networkCard - - property color accentColor: Colours.palette.m3primary - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.large - clip: true - - Ref { - service: NetworkUsage - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small - - CardHeader { - icon: "swap_vert" - title: qsTr("Network") - accentColor: networkCard.accentColor - } - - // Sparkline graph - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - SparklineItem { - id: sparkline - - property real targetMax: 1024 - property real smoothMax: targetMax - - anchors.fill: parent - line1: NetworkUsage.uploadBuffer // qmllint disable missing-type - line1Color: Colours.palette.m3secondary - line1FillAlpha: 0.15 - line2: NetworkUsage.downloadBuffer // qmllint disable missing-type - line2Color: Colours.palette.m3tertiary - line2FillAlpha: 0.2 - maxValue: smoothMax - historyLength: NetworkUsage.historyLength - - Connections { - function onValuesChanged(): void { - sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); - slideAnim.restart(); - } - - target: NetworkUsage.downloadBuffer - } - - NumberAnimation { - id: slideAnim - - target: sparkline - property: "slideProgress" - from: 0 - to: 1 - duration: GlobalConfig.dashboard.resourceUpdateInterval - } - - Behavior on smoothMax { - Anim { - type: Anim.StandardLarge - } - } - } - - // "No data" placeholder - StyledText { - anchors.centerIn: parent - text: qsTr("Collecting data...") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - visible: NetworkUsage.downloadBuffer.count < 2 - opacity: 0.6 - } - } - - // Download row - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "download" - color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: qsTr("Download") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - StyledText { - text: { - const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); - return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; - } - font.pointSize: Tokens.font.size.normal - font.weight: Font.Medium - color: Colours.palette.m3tertiary - } - } - - // Upload row - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "upload" - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: qsTr("Upload") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - StyledText { - text: { - const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); - return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; - } - font.pointSize: Tokens.font.size.normal - font.weight: Font.Medium - color: Colours.palette.m3secondary - } - } - - // Session totals - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.normal - - MaterialIcon { - text: "history" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: qsTr("Total") - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - - Item { - Layout.fillWidth: true - } - - StyledText { - text: { - const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); - const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); - return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; - } - font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - } - } + Layout.fillHeight: true + visible: active } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 5bf99ca3..e67cfd5e 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Templates import Quickshell -import Quickshell.Widgets import Caelestia.Config import qs.components import qs.components.controls @@ -18,7 +18,7 @@ Item { readonly property alias count: bar.count - implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight + implicitHeight: bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight TabBar { id: bar @@ -26,12 +26,21 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top + anchors.topMargin: Tokens.sizes.dashboard.tabIndicatorSpacing currentIndex: root.dashState.currentTab - background: null - onCurrentIndexChanged: root.dashState.currentTab = currentIndex + implicitHeight: contentHeight + background: null + contentItem: RowLayout { + spacing: 0 + + Repeater { + model: bar.contentModel + } + } + Repeater { model: ScriptModel { values: root.tabs @@ -106,6 +115,10 @@ Item { required property string iconName readonly property bool current: TabBar.tabBar.currentItem === this + Layout.fillWidth: true + Layout.preferredWidth: 1 // Uniform width across all tabs + implicitWidth: implicitContentWidth + implicitHeight: implicitContentHeight background: null contentItem: CustomMouseArea { @@ -124,92 +137,20 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor - onPressed: event => { - root.dashState.currentTab = tab.TabBar.index; + onPressed: root.dashState.currentTab = tab.TabBar.index - const stateY = stateWrapper.y; - rippleAnim.x = event.x; - rippleAnim.y = event.y - stateY; - - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y))); - - rippleAnim.restart(); - } - - SequentialAnimation { - id: rippleAnim - - property real x - property real y - property real radius - - PropertyAction { - target: ripple - property: "x" - value: rippleAnim.x - } - PropertyAction { - target: ripple - property: "y" - value: rippleAnim.y - } - PropertyAction { - target: ripple - property: "opacity" - value: 0.08 - } - Anim { - target: ripple - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standardDecel - } - Anim { - target: ripple - property: "opacity" - to: 0 - } - } - - ClippingRectangle { - id: stateWrapper + StateLayer { + id: stateLayer + anchors.fill: undefined anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 - color: "transparent" - radius: Tokens.rounding.small - - StyledRect { - id: stateLayer - - anchors.fill: parent - - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: mouse.pressed ? 0.1 : mouse.containsMouse ? 0.08 : 0 - - Behavior on opacity { - Anim {} - } - } - - StyledRect { - id: ripple - - radius: Tokens.rounding.full - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: 0 - - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 - } - } + radius: Tokens.rounding.medium + color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface + onClicked: root.dashState.currentTab = tab.TabBar.index } MaterialIcon { @@ -221,10 +162,12 @@ Item { text: tab.iconName color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant fill: tab.current ? 1 : 0 - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium Behavior on fill { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/dashboard/WeatherTab.qml b/modules/dashboard/WeatherTab.qml index 3cf8d3b7..b942a8b0 100644 --- a/modules/dashboard/WeatherTab.qml +++ b/modules/dashboard/WeatherTab.qml @@ -17,7 +17,7 @@ Item { id: layout anchors.fill: parent - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium RowLayout { Layout.leftMargin: Tokens.padding.large @@ -25,18 +25,17 @@ Item { Layout.fillWidth: true Column { - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { text: Weather.city || qsTr("Loading...") - font.pointSize: Tokens.font.size.extraLarge - font.weight: 600 + font: Tokens.font.body.builders.large.size(28).weight(Font.DemiBold).build() color: Colours.palette.m3onSurface } StyledText { text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } } @@ -46,7 +45,7 @@ Item { } Row { - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased WeatherStat { icon: "wb_twilight" @@ -66,21 +65,21 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small * 2 + implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small - radius: Tokens.rounding.large * 2 + radius: Tokens.rounding.extraLarge * 2 color: Colours.tPalette.m3surfaceContainer RowLayout { id: bigInfoRow anchors.centerIn: parent - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased MaterialIcon { Layout.alignment: Qt.AlignVCenter text: Weather.icon - font.pointSize: Tokens.font.size.extraLarge * 3 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build() color: Colours.palette.m3secondary animate: true } @@ -91,15 +90,14 @@ Item { StyledText { text: Weather.temp - font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 500 + font: Tokens.font.body.builders.large.size(28 * 2).weight(Font.Medium).build() color: Colours.palette.m3primary } StyledText { - Layout.leftMargin: Tokens.padding.small + Layout.leftMargin: Tokens.padding.extraSmall text: Weather.description - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium color: Colours.palette.m3onSurfaceVariant } } @@ -108,7 +106,7 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium DetailCard { icon: "water_drop" @@ -131,18 +129,17 @@ Item { } StyledText { - Layout.topMargin: Tokens.spacing.normal - Layout.leftMargin: Tokens.padding.normal + Layout.topMargin: Tokens.spacing.medium + Layout.leftMargin: Tokens.padding.medium visible: forecastRepeater.count > 0 text: qsTr("7-Day Forecast") - font.pointSize: Tokens.font.size.normal - font.weight: 600 + font: Tokens.font.body.builders.medium.weight(Font.DemiBold).build() color: Colours.palette.m3onSurface } RowLayout { Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium Repeater { id: forecastRepeater @@ -156,9 +153,9 @@ Item { required property var modelData Layout.fillWidth: true - implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -170,16 +167,15 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") - font.pointSize: Tokens.font.size.normal - font.weight: 600 + font: Tokens.font.body.builders.medium.weight(Font.DemiBold).build() color: Colours.palette.m3primary } StyledText { - Layout.topMargin: -Tokens.spacing.small / 2 + Layout.topMargin: -Tokens.spacing.extraSmall Layout.alignment: Qt.AlignHCenter text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small opacity: 0.7 color: Colours.palette.m3onSurfaceVariant } @@ -187,14 +183,14 @@ Item { MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastItem.modelData.icon - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge color: Colours.palette.m3secondary } StyledText { Layout.alignment: Qt.AlignHCenter text: GlobalConfig.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" - font.weight: 600 + font: Tokens.font.body.builders.small.weight(Font.DemiBold).build() color: Colours.palette.m3tertiary } } @@ -213,17 +209,17 @@ Item { Layout.fillWidth: true Layout.preferredHeight: 60 - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainer Row { anchors.centerIn: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: detailRoot.icon color: detailRoot.colour - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large anchors.verticalCenter: parent.verticalCenter } @@ -233,13 +229,13 @@ Item { StyledText { text: detailRoot.label - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small opacity: 0.7 horizontalAlignment: Text.AlignLeft } StyledText { text: detailRoot.value - font.weight: 600 + font: Tokens.font.body.builders.small.weight(Font.DemiBold).build() horizontalAlignment: Text.AlignLeft } } @@ -258,20 +254,19 @@ Item { MaterialIcon { text: weatherStat.icon - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge color: weatherStat.colour } Column { StyledText { text: weatherStat.label - font.pointSize: Tokens.font.size.smaller + font: Tokens.font.body.small color: Colours.palette.m3onSurfaceVariant } StyledText { text: weatherStat.value - font.pointSize: Tokens.font.size.small - font.weight: 600 + font: Tokens.font.body.builders.small.weight(Font.DemiBold).build() color: Colours.palette.m3onSurface } } diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index f7f03742..5debd7ad 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -12,7 +12,6 @@ Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false readonly property DashboardState dashState: DashboardState { reloadableId: "dashboardState" } @@ -28,7 +27,7 @@ Item { } } - readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property real nonAnimHeight: (content.item as Content)?.nonAnimHeight ?? 0 readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled property real offsetScale: shouldBeActive ? 0 : 1 @@ -39,9 +38,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index e2af3c01..9061f2ee 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import M3Shapes import Caelestia.Config import qs.components import qs.components.controls @@ -36,57 +37,43 @@ CustomMouseArea { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall RowLayout { id: monthNavigationRow Layout.fillWidth: true - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall - Item { - implicitWidth: implicitHeight - implicitHeight: prevMonthText.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: prevMonthStateLayer - - radius: Tokens.rounding.full - onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) - } - - MaterialIcon { - id: prevMonthText - - anchors.centerIn: parent - text: "chevron_left" - color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.normal - font.weight: 700 - } + IconButton { + icon: "chevron_left" + type: IconButton.Text + font: Tokens.font.icon.builders.small.weight(Font.Bold).build() + padding: Tokens.padding.small + onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) } Item { Layout.fillWidth: true + Layout.fillHeight: true - implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.small * 2 - implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 + implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.large * 2 + implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.extraSmall * 2 StateLayer { - onClicked: { - root.dashState.currentDate = new Date(); - } - - anchors.fill: monthYearDisplay - anchors.margins: -Tokens.padding.small - anchors.leftMargin: -Tokens.padding.normal - anchors.rightMargin: -Tokens.padding.normal - - radius: Tokens.rounding.full + color: Colours.palette.m3primary + radius: pressed ? Tokens.rounding.small : Tokens.rounding.large disabled: { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } + onClicked: root.dashState.currentDate = new Date() + + Behavior on radius { + Anim { + type: Anim.DefaultEffects + } + } } StyledText { @@ -95,35 +82,16 @@ CustomMouseArea { anchors.centerIn: parent text: grid.title color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.normal - font.weight: 500 - font.capitalization: Font.Capitalize + font: Tokens.font.title.builders.small.capitalisation(Font.Capitalize).build() } } - Item { - implicitWidth: implicitHeight - implicitHeight: nextMonthText.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: nextMonthStateLayer - - onClicked: { - root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } - - radius: Tokens.rounding.full - } - - MaterialIcon { - id: nextMonthText - - anchors.centerIn: parent - text: "chevron_right" - color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.normal - font.weight: 700 - } + IconButton { + icon: "chevron_right" + type: IconButton.Text + font: Tokens.font.icon.builders.small.weight(Font.Bold).build() + padding: Tokens.padding.small + onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1) } } @@ -138,8 +106,8 @@ CustomMouseArea { horizontalAlignment: Text.AlignHCenter text: model.shortName - font.weight: 500 - color: (model.day === 0 || model.day === 6) ? Colours.palette.m3secondary : Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.builders.small.weight(Font.Medium).build() + color: (model.day === 0 || model.day === 6) ? Colours.palette.m3tertiary : Colours.palette.m3onSurface } } @@ -164,7 +132,7 @@ CustomMouseArea { required property var model implicitWidth: implicitHeight - implicitHeight: text.implicitHeight + Tokens.padding.small * 2 + implicitHeight: text.implicitHeight + Tokens.padding.small StyledText { id: text @@ -174,20 +142,19 @@ CustomMouseArea { horizontalAlignment: Text.AlignHCenter text: grid.locale.toString(dayItem.model.day) color: { - const dayOfWeek = dayItem.model.date.getUTCDay(); + const dayOfWeek = dayItem.model.date.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) - return Colours.palette.m3secondary; + return Colours.palette.m3tertiary; return Colours.palette.m3onSurfaceVariant; } opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 - font.pointSize: Tokens.font.size.normal - font.weight: 500 + font: Tokens.font.body.small } } } - StyledRect { + MaterialShape { id: todayIndicator readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null @@ -199,13 +166,12 @@ CustomMouseArea { } x: today ? today.x + (today.width - implicitWidth) / 2 : 0 - y: today?.y ?? 0 + y: today ? today.y - Tokens.padding.extraSmall - 1 : 0 - implicitWidth: today?.implicitWidth ?? 0 - implicitHeight: today?.implicitHeight ?? 0 + implicitSize: today ? Math.max(today.implicitWidth, today.implicitHeight) + Tokens.padding.extraSmall * 2 : 0 + shape: MaterialShape.Sunny clip: true - radius: Tokens.rounding.full color: Colours.palette.m3primary opacity: todayItem ? 1 : 0 @@ -224,23 +190,23 @@ CustomMouseArea { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { - Anim {} + Anim { + type: Anim.FastSpatial + } } Behavior on x { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on y { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 82bee151..5b7f4bb7 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -24,17 +24,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.hourStr color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge - font.family: Tokens.font.family.clock - font.weight: 600 + font: Tokens.font.clock.size(28).weight(Font.DemiBold).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: "•••" color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.extraLarge * 0.9 - font.family: Tokens.font.family.clock + font: Tokens.font.clock.size(28 * 0.9).build() } StyledText { @@ -42,9 +39,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.minuteStr color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge - font.family: Tokens.font.family.clock - font.weight: 600 + font: Tokens.font.clock.size(28).weight(Font.DemiBold).build() } Loader { @@ -57,9 +52,7 @@ Item { sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.clock - font.weight: 600 + font: Tokens.font.clock.size(18).weight(Font.DemiBold).build() } } } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index a14e5e38..bbd8a351 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,9 +1,12 @@ +pragma ComponentBehavior: Bound + import QtQuick -import QtQuick.Shapes -import Quickshell +import Caelestia.Components import Caelestia.Config import Caelestia.Services import qs.components +import qs.components.controls +import qs.components.widgets import qs.services import qs.utils @@ -15,6 +18,8 @@ Item { return active?.length ? (active.position % active.length) / active.length : 0; } + readonly property real arcCoverGap: Tokens.spacing.extraSmall + anchors.top: parent.top anchors.bottom: parent.bottom implicitWidth: Tokens.sizes.dashboard.mediaWidth @@ -37,84 +42,32 @@ Item { service: Audio.beatTracker } - Shape { - preferredRendererType: Shape.CurveRenderer + CircularProgress { + id: prog - ShapePath { - fillColor: "transparent" - strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + anchors.centerIn: cover + implicitSize: cover.width + root.arcCoverGap + thickness * 2 - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 - sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep - } + fgColour: Colours.palette.m3primary + strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness + startAngle: -90 - sweepAngle / 2 + sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep + value: root.playerProgress - Behavior on strokeColor { - CAnim {} - } - } - - ShapePath { - fillColor: "transparent" - strokeColor: Colours.palette.m3primary - strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small - startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 - sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress - } - - Behavior on strokeColor { - CAnim {} - } - } + wavy: true + waveFrequency: 8 + waveDuration: 2000 + wavePaused: !Players.active?.isPlaying } - StyledClippingRect { + CoverArt { id: cover anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small - + anchors.margins: Tokens.padding.medium + root.arcCoverGap + prog.thickness implicitHeight: width - color: Colours.tPalette.m3surfaceContainerHigh - radius: Infinity - - MaterialIcon { - anchors.centerIn: parent - - grade: 200 - text: "art_track" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: (parent.width * 0.4) || 1 - } - - Image { - id: image - - anchors.fill: parent - - source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize: { - const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; - return Qt.size(width * dpr, height * dpr); - } - } } StyledText { @@ -122,15 +75,15 @@ Item { anchors.top: cover.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Tokens.spacing.normal + anchors.topMargin: Tokens.spacing.medium animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small - width: parent.implicitWidth - Tokens.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.extraLargeIncreased elide: Text.ElideRight } @@ -145,9 +98,9 @@ Item { horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small - width: parent.implicitWidth - Tokens.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.extraLargeIncreased elide: Text.ElideRight } @@ -163,34 +116,46 @@ Item { text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") color: Colours.palette.m3secondary - width: parent.implicitWidth - Tokens.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.extraLargeIncreased elide: Text.ElideRight } - Row { + ButtonRow { id: controls anchors.top: artist.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Tokens.spacing.smaller + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Tokens.spacing.medium + anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoPrevious onClicked: Players.active?.previous() } - PlayerControl { + IconButton { + fillWidth: true icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false + isRound: true + shapeMorph: true + checked: Players.active?.isPlaying ?? false + disabled: !Players.active?.canTogglePlaying onClicked: Players.active?.togglePlaying() } - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_next" - canUse: Players.active?.canGoNext ?? false + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } } @@ -204,7 +169,7 @@ Item { anchors.right: parent.right anchors.topMargin: Tokens.spacing.small anchors.bottomMargin: Tokens.padding.large - anchors.margins: Tokens.padding.large * 2 + anchors.margins: Tokens.padding.extraLargeIncreased playing: Players.active?.isPlaying ?? false speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type @@ -212,34 +177,4 @@ Item { asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } - - component PlayerControl: StyledRect { - id: control - - required property string icon - required property bool canUse - - signal clicked - - implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small - implicitHeight: implicitWidth - - StateLayer { - disabled: !control.canUse - radius: Tokens.rounding.full - onClicked: control.clicked() - } - - MaterialIcon { - id: icon - - anchors.centerIn: parent - anchors.verticalCenterOffset: font.pointSize * 0.05 - - animate: true - text: control.icon - color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline - font.pointSize: Tokens.font.size.large - } - } } diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index 2e0f085b..898f69a4 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,87 +1,75 @@ import QtQuick +import QtQuick.Layouts import Caelestia.Config +import Caelestia.Services import qs.components -import qs.components.misc +import qs.components.controls import qs.services -Row { +Item { id: root anchors.top: parent.top anchors.bottom: parent.bottom - padding: Tokens.padding.large - spacing: Tokens.spacing.normal + implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 - Ref { - service: SystemUsage + ServiceRef { + service: Cpu } - Resource { - icon: "memory" - value: SystemUsage.cpuPerc - colour: Colours.palette.m3primary + ServiceRef { + service: Memory } - Resource { - icon: "memory_alt" - value: SystemUsage.memPerc - colour: Colours.palette.m3secondary + ServiceRef { + service: Storage } - Resource { - icon: "hard_disk" - value: SystemUsage.storagePerc - colour: Colours.palette.m3tertiary - } - - component Resource: Item { - id: res - - required property string icon - required property real value - required property color colour + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: Tokens.padding.large - implicitWidth: icon.implicitWidth + spacing: Tokens.spacing.medium - StyledRect { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: icon.top - anchors.bottomMargin: Tokens.spacing.small + Resource { + icon: "memory" + value: Cpu.percentage + } - implicitWidth: Tokens.sizes.dashboard.resourceProgressThickness + Resource { + icon: "memory_alt" + value: Memory.percentage + fgColour: Colours.palette.m3tertiary + } - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.full + Resource { + icon: "hard_disk" + value: Storage.percentage + fgColour: Colours.palette.m3secondary + } + } + component Resource: CircularProgress { + id: res - StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - implicitHeight: res.value * parent.height + required property string icon - color: res.colour - radius: Tokens.rounding.full - } + Layout.fillHeight: true + implicitSize: height + strokeWidth: Tokens.sizes.dashboard.resourceProgressThickness + + Behavior on clampedVal { + Anim {} } MaterialIcon { - id: icon - - anchors.bottom: parent.bottom - + anchors.centerIn: parent text: res.icon - color: res.colour - } - - Behavior on value { - Anim { - type: Anim.StandardLarge - } + font: Tokens.font.icon.large + color: res.fgColour } } } diff --git a/modules/dashboard/dash/SmallWeather.qml b/modules/dashboard/dash/SmallWeather.qml index 998c7125..cd5f3e40 100644 --- a/modules/dashboard/dash/SmallWeather.qml +++ b/modules/dashboard/dash/SmallWeather.qml @@ -9,6 +9,7 @@ Item { anchors.centerIn: parent implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin + implicitHeight: Math.max(icon.implicitHeight, info.implicitHeight) + Tokens.padding.largeIncreased * 2 Component.onCompleted: Weather.reload() @@ -21,7 +22,7 @@ Item { animate: true text: Weather.icon color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge * 2 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(1.6).build() } Column { @@ -29,9 +30,9 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.large + anchors.leftMargin: Tokens.spacing.largeIncreased - spacing: Tokens.spacing.small + spacing: Tokens.spacing.extraSmall StyledText { anchors.horizontalCenter: parent.horizontalCenter @@ -39,8 +40,7 @@ Item { animate: true text: Weather.temp color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 + font: Tokens.font.headline.builders.medium.width(110).weight(Font.DemiBold).build() } StyledText { @@ -48,9 +48,10 @@ Item { animate: true text: Weather.description + font: Tokens.font.body.small elide: Text.ElideRight - width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.large * 2) + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.extraLargeIncreased) } } } diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 79787de5..cd0a96ef 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,4 +1,7 @@ +pragma ComponentBehavior: Bound + import QtQuick +import M3Shapes import Caelestia.Config import qs.components import qs.components.effects @@ -7,186 +10,268 @@ import qs.components.images import qs.services import qs.utils -Row { +Item { id: root required property DrawerVisibilities visibilities required property FileDialog facePicker - padding: Tokens.padding.large - spacing: Tokens.spacing.normal + property color pfpFallbackColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - StyledClippingRect { - implicitWidth: info.implicitHeight - implicitHeight: info.implicitHeight + anchors.fill: parent + anchors.margins: Tokens.padding.large - radius: Tokens.rounding.large - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + Behavior on pfpFallbackColour { + CAnim {} + } + + Item { + id: pfpContainer + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: logoShape.right + anchors.leftMargin: -(Tokens.padding.largeIncreased + Tokens.padding.extraLarge) / 2 + implicitWidth: height + + MaterialShape { + id: shape - MaterialIcon { anchors.centerIn: parent + implicitSize: parent.height + shape: MaterialShape.Pill + color: Qt.alpha(root.pfpFallbackColour, 1) + opacity: root.pfpFallbackColour.a + layer.enabled: true - text: "person" - fill: 1 - grade: 200 - font.pointSize: Math.floor(info.implicitHeight / 2) || 1 - visible: pfp.status !== Image.Ready + MouseArea { + id: mouse + + containmentMask: QtObject { + function contains(pt: point): bool { + return shape.contains(pt) && !logoShape.contains(mouse.mapToItem(logoShape, pt)) && !uptimeShape.contains(mouse.mapToItem(uptimeShape, pt)); + } + } + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.visibilities.dashboard = false; + root.facePicker.open(); + } + } } - CachingImage { - id: pfp - + Item { anchors.fill: parent - path: `${Paths.home}/.face` - } + layer.enabled: true + layer.effect: Mask { + maskSource: shape + } - MouseArea { - anchors.fill: parent - hoverEnabled: true + Loader { + anchors.centerIn: parent + asynchronous: true + active: pfp.status !== Image.Ready + + sourceComponent: MaterialIcon { + text: "person_add" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.extraLarge + fill: 1 + grade: -2 // Ugh material symbols are such a pain with fill + } + } + + CachingImage { + id: pfp + + anchors.fill: parent + path: `${Paths.home}/.face` + } StyledRect { anchors.fill: parent - - color: Qt.alpha(Colours.palette.m3scrim, 0.5) - opacity: parent.containsMouse ? 1 : 0 + color: Qt.alpha(Colours.palette.m3scrim, pfp.status === Image.Ready ? 0.4 : 0) + opacity: mouse.containsMouse ? 1 : 0 + layer.enabled: opacity < 1 Behavior on opacity { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - } - } - } - - StyledRect { - anchors.centerIn: parent - - implicitWidth: selectIcon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: selectIcon.implicitHeight + Tokens.padding.small * 2 - - radius: Tokens.rounding.normal - color: Colours.palette.m3primary - scale: parent.containsMouse ? 1 : 0.5 - opacity: parent.containsMouse ? 1 : 0 - - StateLayer { - color: Colours.palette.m3onPrimary - onClicked: { - root.visibilities.launcher = false; - root.facePicker.open(); + type: Anim.DefaultEffects } } - MaterialIcon { - id: selectIcon - + MaterialShape { anchors.centerIn: parent - anchors.horizontalCenterOffset: -font.pointSize * 0.02 + implicitSize: parent.height * 0.7 + shape: MaterialShape.Diamond + color: Colours.palette.m3primary + scale: mouse.pressed ? 0.9 : mouse.containsMouse ? 1 : 0.7 - text: "frame_person" - color: Colours.palette.m3onPrimary - font.pointSize: Tokens.font.size.extraLarge - } - - Behavior on scale { - Anim { - type: Anim.FastSpatial + Behavior on color { + CAnim {} } - } - Behavior on opacity { - Anim { - duration: Tokens.anim.durations.expressiveFastSpatial + Behavior on scale { + Anim { + type: Anim.FastSpatial + } + } + + MaterialIcon { + anchors.centerIn: parent + text: "person_edit" + color: Colours.palette.m3onPrimary + fontStyle: Tokens.font.icon.large } } } } } - Column { - id: info + MaterialShape { + id: logoShape - anchors.verticalCenter: parent.verticalCenter - spacing: Tokens.spacing.normal + x: Tokens.padding.extraSmall + implicitSize: Tokens.sizes.dashboard.logoSize + Tokens.padding.small * 2 + shape: MaterialShape.Gem + color: Colours.palette.m3primaryContainer - Item { - id: line + Behavior on color { + CAnim {} + } - implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) + Loader { + anchors.centerIn: parent + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : osLogo + } + } - ColouredIcon { - id: icon + Component { + id: osLogo - anchors.left: parent.left - anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 + ColouredIcon { + id: icon - source: SysInfo.osLogo - implicitSize: Math.floor(Tokens.font.size.normal * 1.34) - colour: Colours.palette.m3primary + source: SysInfo.osLogo + implicitSize: Tokens.sizes.dashboard.logoSize + colour: Colours.palette.m3onPrimaryContainer + } + } + + Component { + id: caelestiaLogo + + Logo { + implicitWidth: Tokens.sizes.dashboard.logoSize + implicitHeight: Tokens.sizes.dashboard.logoSize + topColour: Colours.palette.m3primary + bottomColour: Colours.palette.m3onPrimaryContainer + } + } + + MaterialShape { + id: uptimeShape + + anchors.bottom: parent.bottom + anchors.left: pfpContainer.right + anchors.bottomMargin: -Tokens.padding.small // Clamshell is taller than what it is visually + anchors.leftMargin: -Tokens.padding.extraLargeIncreased + implicitSize: Tokens.sizes.dashboard.uptimeSize + Tokens.padding.small * 2 + shape: MaterialShape.ClamShell + color: Colours.palette.m3tertiaryContainer + + Behavior on color { + CAnim {} + } + + MaterialIcon { + anchors.centerIn: parent + text: "clock_arrow_up" + color: Colours.palette.m3onTertiaryContainer + fontStyle: Tokens.font.icon.medium + } + } + + StyledText { + anchors.left: uptimeShape.right + anchors.verticalCenter: uptimeShape.verticalCenter + anchors.leftMargin: Tokens.spacing.small + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.1) + + text: "up " + SysInfo.uptime.split(",").slice(0, 2).join(",") // Max 2 components + font: Tokens.font.label.medium + width: Tokens.sizes.dashboard.userWidth - x - Tokens.padding.extraLarge + elide: Text.ElideRight + } + + StyledRect { + id: bubble1 + + anchors.left: pfpContainer.right + anchors.top: bubble2.bottom + anchors.leftMargin: Tokens.spacing.small + anchors.topMargin: -Tokens.spacing.extraSmall + + implicitWidth: 10 + implicitHeight: 10 + radius: Tokens.rounding.full + color: Colours.palette.m3secondaryContainer + } + + StyledRect { + id: bubble2 + + anchors.left: bubble1.right + anchors.verticalCenter: wmContainer.bottom + anchors.leftMargin: Tokens.spacing.extraSmall + + implicitWidth: 15 + implicitHeight: 15 + radius: Tokens.rounding.full + color: Colours.palette.m3secondaryContainer + } + + StyledRect { + id: wmContainer + + anchors.left: bubble2.left + anchors.leftMargin: -Tokens.padding.medium + y: Tokens.padding.extraSmall + + radius: Tokens.rounding.largeIncreased + color: Colours.palette.m3secondaryContainer + implicitWidth: wmLabel.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: wmLabel.implicitHeight + Tokens.padding.small * 2 + + Row { + id: wmLabel + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + id: wmIcon + + anchors.verticalCenter: parent.verticalCenter + text: "select_window" + color: Colours.palette.m3onSecondaryContainer + fontStyle: wmText.font } StyledText { - id: text + id: wmText - anchors.verticalCenter: icon.verticalCenter - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${SysInfo.osPrettyName || SysInfo.osName}` - font.pointSize: Tokens.font.size.normal - - width: Tokens.sizes.dashboard.infoWidth + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.1) + text: SysInfo.wm + "..." + color: Colours.palette.m3onSecondaryContainer + font: Tokens.font.body.builders.small.vaxis("slnt", -4).build() + width: Math.min(implicitWidth, Tokens.sizes.dashboard.userWidth - wmContainer.x - Tokens.padding.medium * 2 - wmIcon.implicitWidth - wmLabel.spacing - Tokens.padding.extraLarge) elide: Text.ElideRight } } - - InfoLine { - icon: "select_window_2" - text: SysInfo.wm - colour: Colours.palette.m3secondary - } - - InfoLine { - id: uptime - - icon: "timer" - text: qsTr("up %1").arg(SysInfo.uptime) - colour: Colours.palette.m3tertiary - } - } - - component InfoLine: Item { - id: line - - required property string icon - required property string text - required property color colour - - implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) - - MaterialIcon { - id: icon - - anchors.left: parent.left - anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 - - fill: 1 - text: line.icon - color: line.colour - font.pointSize: Tokens.font.size.normal - } - - StyledText { - id: text - - anchors.verticalCenter: icon.verticalCenter - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${line.text}` - font.pointSize: Tokens.font.size.normal - - width: Tokens.sizes.dashboard.infoWidth - elide: Text.ElideRight - } } } diff --git a/modules/dashboard/media/BackgroundShapes.qml b/modules/dashboard/media/BackgroundShapes.qml new file mode 100644 index 00000000..8523b9b1 --- /dev/null +++ b/modules/dashboard/media/BackgroundShapes.qml @@ -0,0 +1,97 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import M3Shapes +import qs.components +import qs.services + +Item { + id: root + + readonly property list shapePool: [MaterialShape.Circle, MaterialShape.Cookie4Sided, MaterialShape.Cookie6Sided, MaterialShape.Cookie7Sided, MaterialShape.Cookie9Sided, MaterialShape.Cookie12Sided, MaterialShape.Sunny, MaterialShape.VerySunny, MaterialShape.SoftBurst, MaterialShape.Pentagon, MaterialShape.Gem, MaterialShape.Arch, MaterialShape.Arrow, MaterialShape.Pill, MaterialShape.Triangle, MaterialShape.Fan, MaterialShape.Oval] + + property int count: 14 + property real minSize: 36 + property real maxSize: 124 + property real minSpeed: 4 + property real maxSpeed: 18 + property real minRotSpeed: -12 + property real maxRotSpeed: 12 + property list lightOpacities: [0.34, 0.34, 0.08, 0.2] + property list darkOpacities: [0.16, 0.16, 0.04, 0.16] + + function rand(min: real, max: real): real { + return min + Math.random() * (max - min); + } + + function signedRand(min: real, max: real): real { + return rand(min, max) * (Math.random() < 0.5 ? -1 : 1); + } + + clip: true + Component.onCompleted: shapes.model = count + + Repeater { + id: shapes + + DriftingShape {} + } + + FrameAnimation { + running: root.visible && root.width > 0 && root.height > 0 && (Players.active?.isPlaying ?? false) + onTriggered: { + const dt = frameTime; + for (let i = 0; i < shapes.count; i++) { + const s = shapes.itemAt(i) as DriftingShape; + if (!s) + continue; + + s.x += s.vx * dt; + s.y += s.vy * dt; + s.rotation += s.vr * dt; + + if (s.x + s.width < 0) + s.x = root.width; + else if (s.x > root.width) + s.x = -s.width; + + if (s.y + s.height < 0) + s.y = root.height; + else if (s.y > root.height) + s.y = -s.height; + } + } + } + + component DriftingShape: MaterialShape { + id: shapeItem + + required property int index + + property real vx: root.signedRand(root.minSpeed, root.maxSpeed) + property real vy: root.signedRand(root.minSpeed, root.maxSpeed) + property real vr: root.rand(root.minRotSpeed, root.maxRotSpeed) + readonly property int colourIdx: Math.floor(Math.random() * 4) + + implicitSize: root.minSize + (index / root.count) * (root.maxSize - root.minSize) + shape: root.shapePool[Math.floor(Math.random() * root.shapePool.length)] + color: [Colours.palette.m3primaryContainer, Colours.palette.m3secondaryContainer, Colours.palette.m3tertiaryContainer, Colours.palette.m3outlineVariant][colourIdx] + opacity: Colours.light ? root.lightOpacities[colourIdx] : root.darkOpacities[colourIdx] + rotation: root.rand(0, 360) + + Component.onCompleted: { + x = root.rand(0, root.width - implicitSize); + y = root.rand(0, root.height - implicitSize); + } + + Behavior on color { + CAnim {} + } + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } +} diff --git a/modules/dashboard/media/CoverVisualiser.qml b/modules/dashboard/media/CoverVisualiser.qml new file mode 100644 index 00000000..e642b466 --- /dev/null +++ b/modules/dashboard/media/CoverVisualiser.qml @@ -0,0 +1,82 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import Quickshell +import M3Shapes +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.widgets +import qs.services + +Item { + id: root + + readonly property real centerX: width / 2 + readonly property real centerY: height / 2 + readonly property real spacing: Tokens.spacing.medium + readonly property real maxMagnitude: (implicitWidth - cover.implicitWidth) / 2 - spacing + + ServiceRef { + service: Audio.cava + } + + Shape { + anchors.fill: parent + asynchronous: true + preferredRendererType: Shape.CurveRenderer + data: bars.instances + } + + Variants { + id: bars + + model: Array.from({ + length: GlobalConfig.services.visualiserBars + }, (_, i) => i) + + ShapePath { + id: bar + + required property int modelData + readonly property real value: Math.max(1e-2, Math.min(1, Audio.cava.values[modelData])) + + readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars + readonly property real dist: shapeEdgeDist + value * root.maxMagnitude + readonly property real shapeEdgeDist: { + cover.shape.rotation; // Update when shape rotation changes + const sDist = cover.shape.distanceAtAngle(modelData * 360 / GlobalConfig.services.visualiserBars + 90); + return sDist + root.spacing + strokeWidth / 2; + } + readonly property real cos: Math.cos(angle) + readonly property real sin: Math.sin(angle) + + asynchronous: true + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 + strokeColor: Colours.palette.m3primary + + startX: root.centerX + shapeEdgeDist * cos + startY: root.centerY + shapeEdgeDist * sin + + PathLine { + x: root.centerX + bar.dist * bar.cos + y: root.centerY + bar.dist * bar.sin + } + + Behavior on strokeColor { + CAnim {} + } + } + } + + CoverArt { + id: cover + + anchors.centerIn: parent + shape.shape: MaterialShape.Cookie9Sided + implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize + implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize + } +} diff --git a/modules/dashboard/media/Details.qml b/modules/dashboard/media/Details.qml new file mode 100644 index 00000000..2e15a7b0 --- /dev/null +++ b/modules/dashboard/media/Details.qml @@ -0,0 +1,180 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Mpris +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +ColumnLayout { + id: root + + function lengthStr(length: int): string { + if (length < 0) + return "-1:-1"; + + const hours = Math.floor(length / 3600); + const mins = Math.floor((length % 3600) / 60); + const secs = Math.floor(length % 60).toString().padStart(2, "0"); + + if (hours > 0) + return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; + return `${mins}:${secs}`; + } + + spacing: Tokens.spacing.extraSmall + + Timer { + running: Players.active?.isPlaying ?? false + interval: GlobalConfig.dashboard.mediaUpdateInterval + triggeredOnStart: true + repeat: true + onTriggered: Players.active?.positionChanged() + } + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackTitle ?? "" + font: Tokens.font.title.large + elide: Text.ElideRight + animate: true + } + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackArtist || qsTr("Unknown artist") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.title.medium + elide: Text.ElideRight + animate: true + } + + StyledText { + Layout.fillWidth: true + text: Players.active?.trackAlbum || qsTr("Unknown album") + color: Colours.palette.m3secondary + font: Tokens.font.title.medium + elide: Text.ElideRight + animate: true + } + + RowLayout { + Layout.topMargin: Tokens.spacing.extraLargeIncreased + Layout.fillWidth: true + spacing: Tokens.spacing.small + + TextMetrics { + id: timeMetrics + + text: Players.active ? root.lengthStr(Math.max(Players.active.position, Players.active.length)).replace(/[1-9]/g, "0") : "00:00" + font: Tokens.font.label.medium + } + + StyledText { + Layout.preferredWidth: timeMetrics.width + text: root.lengthStr(Players.active?.position ?? -1) + color: Colours.palette.m3onSurfaceVariant + font: timeMetrics.font + horizontalAlignment: Text.AlignHCenter + } + + StyledSlider { + Layout.fillWidth: true + value: Players.active ? Players.active.position / (Players.active.length || 1) : 0 + enabled: Players.active?.canSeek ?? false + wavy: true + animateWave: Players.active?.isPlaying ?? false + waveFrequency: 5 + waveDuration: 2000 + onInteraction: value => { + const active = Players.active; + if (active?.canSeek && active?.positionSupported) + active.position = value * active.length; + } + } + + StyledText { + Layout.preferredWidth: timeMetrics.width + text: root.lengthStr(Players.active?.length ?? -1) + color: Colours.palette.m3onSurfaceVariant + font: timeMetrics.font + horizontalAlignment: Text.AlignHCenter + } + } + + ButtonRow { + Layout.topMargin: Tokens.spacing.largeIncreased + Layout.fillWidth: true + spacing: Tokens.spacing.extraSmall + + IconButton { + type: IconButton.Tonal + icon: "shuffle" + isRound: true + shapeMorph: true + checked: Players.active?.shuffle ?? false + font: Tokens.font.icon.builders.medium.weight(Font.Medium).build() + disabled: !Players.active?.shuffleSupported + onClicked: Players.active.shuffle = !Players.active?.shuffle + implicitWidth: Math.round(implicitHeight * 0.9) + } + + IconButton { + id: previousBtn + + type: IconButton.Tonal + icon: "skip_previous" + isRound: true + shapeMorph: true + font: Tokens.font.icon.large + disabled: !Players.active?.canGoPrevious + onClicked: Players.active?.previous() + } + + IconButton { + id: playPauseBtn + + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + isRound: true + shapeMorph: true + fillWidth: true + checked: Players.active?.isPlaying ?? false + font: Tokens.font.icon.large + disabled: !Players.active?.canTogglePlaying + onClicked: Players.active?.togglePlaying() + } + + IconButton { + id: nextBtn + + type: IconButton.Tonal + icon: "skip_next" + isRound: true + shapeMorph: true + font: Tokens.font.icon.large + disabled: !Players.active?.canGoNext + onClicked: Players.active?.next() + } + + IconButton { + type: IconButton.Tonal + icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat" + isRound: true + shapeMorph: true + checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist + font: Tokens.font.icon.builders.medium.weight(Font.Medium).build() + disabled: !Players.active?.loopSupported + onClicked: { + const state = Players.active.loopState; + if (state === MprisLoopState.None) + Players.active.loopState = MprisLoopState.Track; + else if (state === MprisLoopState.Track) + Players.active.loopState = MprisLoopState.Playlist; + else + Players.active.loopState = MprisLoopState.None; + } + implicitWidth: Math.round(implicitHeight * 0.9) + } + } +} diff --git a/modules/dashboard/media/LyricList.qml b/modules/dashboard/media/LyricList.qml new file mode 100644 index 00000000..0d44293c --- /dev/null +++ b/modules/dashboard/media/LyricList.qml @@ -0,0 +1,313 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.containers +import qs.components.controls +import qs.components.effects +import qs.services + +Item { + id: root + + // Funny binding hack to make lyrics update + readonly property var _: { + const p = Players.active; + if (p) + Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length); + else + Lyrics.clearTrack(); + } + + readonly property real fadeAmount: 0.1 + property bool flag + property list lyricList: Lyrics.lyrics + + layer.enabled: true + layer.effect: Mask { + maskSource: mask + + Rectangle { + id: mask + + layer.enabled: true + visible: false + implicitWidth: root.width + implicitHeight: root.height + + gradient: Gradient { + orientation: Gradient.Vertical + + GradientStop { + color: Qt.alpha("black", 0) + position: 0 + } + GradientStop { + color: Qt.alpha("black", 1) + position: root.fadeAmount + } + GradientStop { + color: Qt.alpha("black", 1) + position: 1 - root.fadeAmount + } + GradientStop { + color: Qt.alpha("black", 0) + position: 1 + } + } + } + } + + state: { + flag; // For some reason it doesn't update sometimes, so use this to force an update + if (Lyrics.hasLyrics) + return "hasLyrics"; + if (Lyrics.loading) + return "loading"; + return "noLyrics"; + } + + states: [ + State { + name: "loading" + + PropertyChanges { + loadingIndicator.opacity: 1 + lyrics.opacity: 0 + noLyrics.opacity: 0 + } + }, + State { + name: "hasLyrics" + + PropertyChanges { + loadingIndicator.opacity: 0 + lyrics.opacity: 1 + noLyrics.opacity: 0 + } + }, + State { + name: "noLyrics" + + PropertyChanges { + loadingIndicator.opacity: 0 + lyrics.opacity: 0 + noLyrics.opacity: 1 + } + } + ] + + transitions: [ + Transition { + from: "loading" + + SequentialAnimation { + Anim { + target: loadingIndicator + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + targets: [lyrics, noLyrics] + property: "opacity" + type: Anim.SlowEffects + } + } + }, + Transition { + from: "hasLyrics" + + SequentialAnimation { + Anim { + target: lyrics + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + targets: [loadingIndicator, noLyrics] + property: "opacity" + type: Anim.SlowEffects + } + } + }, + Transition { + from: "noLyrics" + + SequentialAnimation { + Anim { + target: noLyrics + property: "opacity" + type: Anim.DefaultEffects + } + Anim { + targets: [loadingIndicator, lyrics] + property: "opacity" + type: Anim.SlowEffects + } + } + } + ] + + Connections { + function onHasLyricsChanged() { + root.flag = !root.flag; + } + + target: Lyrics + } + + Loader { + id: loadingIndicator + + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: 0 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.large + + StyledRect { + Layout.alignment: Qt.AlignHCenter + implicitWidth: shape.implicitSize + Tokens.padding.medium * 2 + implicitHeight: shape.implicitSize + Tokens.padding.medium * 2 + color: Colours.palette.m3primaryContainer + radius: Tokens.rounding.full + + LoadingIndicator { + id: shape + + anchors.centerIn: parent + implicitSize: Math.round(Tokens.sizes.dashboard.mediaSectionWidth / 5) + containsIcon: true // This removes the pentagon, which is not centered + } + } + + StyledText { + text: qsTr("Loading lyrics...") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.title.medium + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + id: noLyrics + + anchors.centerIn: parent + asynchronous: true + active: opacity > 0 + opacity: 0 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "sentiment_sad" + fontStyle: Tokens.font.icon.builders.large.scale(2).build() + color: Colours.palette.m3outline + } + + StyledText { + text: qsTr("No lyrics found") + color: Colours.palette.m3outline + font: Tokens.font.title.medium + } + } + } + + StyledListView { + id: lyrics + + anchors.fill: parent + anchors.topMargin: parent.height * root.fadeAmount / 2 + anchors.bottomMargin: parent.height * root.fadeAmount / 2 + + displayMarginBeginning: anchors.topMargin + displayMarginEnd: anchors.bottomMargin + + model: root.lyricList + Component.onCompleted: { + currentIndex = Qt.binding(() => { + model; // Force update when lyrics change + return Lyrics.indexForTime(Players.active?.position ?? 0); + }); + positionViewAtIndex(currentIndex, ListView.Center); + } + onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)) + + highlightRangeMode: ListView.ApplyRange + highlightMoveDuration: Tokens.anim.durations.large + highlightMoveVelocity: -1 + preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2 + preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2 + + spacing: Tokens.spacing.small + opacity: 0 + + delegate: StyledText { + id: lyric + + required property string modelData + property real effectScale: ListView.isCurrentItem ? 1 : 0 + + anchors.left: ListView.view?.contentItem.left + anchors.right: ListView.view?.contentItem.right + + text: modelData || ". . ." + color: ListView.isCurrentItem ? Colours.palette.m3primary : Colours.palette.m3outline + font: Tokens.font.body.medium + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + layer.enabled: effectScale > 0 + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Colours.palette.m3primary + shadowOpacity: 0.5 * lyric.effectScale + shadowBlur: 0.6 * lyric.effectScale + blur: 0.4 * lyric.effectScale + } + + Behavior on effectScale { + Anim { + type: Anim.SlowEffects + } + } + } + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + + Behavior on lyricList { + SequentialAnimation { + Anim { + target: lyrics + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + PropertyAction {} + Anim { + target: lyrics + property: "opacity" + to: 1 + type: Anim.SlowEffects + } + } + } +} diff --git a/modules/dashboard/media/LyricsAndSelector.qml b/modules/dashboard/media/LyricsAndSelector.qml new file mode 100644 index 00000000..89d7ce5f --- /dev/null +++ b/modules/dashboard/media/LyricsAndSelector.qml @@ -0,0 +1,79 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Mpris +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +Item { + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.leftMargin: Tokens.padding.medium + spacing: Tokens.spacing.medium + + RowLayout { + Layout.bottomMargin: -Tokens.spacing.medium + spacing: Tokens.spacing.medium + z: 1 + + MaterialIcon { + Layout.topMargin: Math.round(fontInfo.pointSize * 0.12) + text: "lyrics" + fontStyle: Tokens.font.icon.medium + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Lyrics") + font: Tokens.font.title.medium + } + + LyricsInfo {} + } + + LyricList { + Layout.fillWidth: true + Layout.fillHeight: true + } + + SplitButton { + Layout.alignment: Qt.AlignHCenter + + type: SplitButton.Tonal + disabled: !Players.list.length + active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData + + menuItems: playerList.instances + fallbackIcon: "music_off" + fallbackText: qsTr("No players") + + minLeftWidth: layout.width - expandBtn.implicitWidth - spacing + label.Layout.maximumWidth: minLeftWidth - iconLabel.implicitWidth - textRow.spacing - textRow.anchors.horizontalCenterOffset / 2 - horizontalPadding * 2 + label.elide: Text.ElideRight + + stateLayer.disabled: true + menuOnTop: true + + Variants { + id: playerList + + model: Players.list + + PlayerItem {} + } + } + } + + component PlayerItem: MenuItem { + required property MprisPlayer modelData + + icon: modelData === Players.active ? "check" : "" + text: Players.getIdentity(modelData) + activeIcon: "animated_images" + } +} diff --git a/modules/dashboard/media/LyricsInfo.qml b/modules/dashboard/media/LyricsInfo.qml new file mode 100644 index 00000000..2c78f9bd --- /dev/null +++ b/modules/dashboard/media/LyricsInfo.qml @@ -0,0 +1,208 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Blobs +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.services + +Item { + id: root + + property bool open + readonly property real padding: Tokens.padding.large + + implicitWidth: btn.implicitWidth * 0.9 + implicitHeight: btn.implicitHeight * 0.9 + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surfaceContainerHighest + smoothing: 16 + cornerFill: false + + Behavior on color { + CAnim {} + } + } + + BlobRect { + id: btnRect + + anchors.fill: parent + anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0 + group: blobGroup + radius: Tokens.rounding.medium + + Behavior on anchors.margins { + Anim {} + } + } + + BlobRect { + id: rect + + anchors.right: parent.right + anchors.top: parent.top + + implicitWidth: parent.width + implicitHeight: parent.height + + group: blobGroup + radius: Tokens.rounding.medium + deformScale: 0.00001 + + states: State { + name: "open" + when: root.open + + PropertyChanges { + rect.anchors.rightMargin: root.width - root.Tokens.spacing.small + rect.anchors.topMargin: -root.Tokens.padding.medium + rect.implicitWidth: Math.max(layout.implicitWidth, placeholder.implicitWidth) + root.padding * 2 + rect.implicitHeight: Math.max(layout.implicitHeight, placeholder.implicitHeight) + root.padding * 2 + content.opacity: 1 + } + } + + transitions: Transition { + Anim { + properties: "rightMargin,implicitWidth" + } + Anim { + properties: "topMargin,implicitHeight" + easing: root.Tokens.anim.expressiveFastSpatial + } + Anim { + property: "opacity" + type: Anim.DefaultEffects + } + } + + Behavior on implicitWidth { + Anim {} + } + + Behavior on implicitHeight { + Anim {} + } + + Item { + id: content + + anchors.fill: parent + clip: true + opacity: 0 + state: Lyrics.loading || !Lyrics.hasLyrics ? "" : "hasLyrics" + + states: State { + name: "hasLyrics" + + PropertyChanges { + layout.opacity: 1 + placeholder.opacity: 0 + } + } + + transitions: [ + Transition { + from: "hasLyrics" + + SequentialAnimation { + Anim { + target: layout + property: "opacity" + type: Anim.FastEffects + } + Anim { + target: placeholder + property: "opacity" + type: Anim.DefaultEffects + } + } + }, + Transition { + to: "hasLyrics" + + SequentialAnimation { + Anim { + target: placeholder + property: "opacity" + type: Anim.FastEffects + } + Anim { + target: layout + property: "opacity" + type: Anim.DefaultEffects + } + } + } + ] + + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + opacity: 0 + + StyledText { + text: qsTr("Backend: %1").arg(LyricsBackend.toString(Lyrics.backend)) + color: Colours.palette.m3onSurfaceVariant + animate: true + } + + StyledText { + Layout.maximumWidth: Tokens.sizes.dashboard.mediaTabWidth / 2 + text: qsTr("Selected candidate: %1 | %2 | %3").arg(Lyrics.selectedCandidate.title).arg(Lyrics.selectedCandidate.artist).arg(Lyrics.selectedCandidate.album) + color: Colours.palette.m3onSurfaceVariant + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + animate: true + } + + StyledText { + text: qsTr("Offset: %1 ms").arg(Lyrics.offset) + color: Colours.palette.m3onSurfaceVariant + animate: true + } + } + + Item { + id: placeholder + + anchors.centerIn: parent + implicitWidth: placeholderText.implicitWidth + implicitHeight: placeholderText.implicitHeight + + StyledText { + id: placeholderText + + text: Lyrics.loading ? qsTr("Loading...") : qsTr("No lyrics found") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + animate: true + } + } + } + } + + MouseArea { + id: btn + + anchors.centerIn: parent + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall * 2 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: root.open = !root.open + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: "more_vert" + fontStyle: Tokens.font.icon.medium + } + } +} diff --git a/modules/dashboard/performance/BatteryTank.qml b/modules/dashboard/performance/BatteryTank.qml new file mode 100644 index 00000000..691405db --- /dev/null +++ b/modules/dashboard/performance/BatteryTank.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.UPower +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.services + +StyledClippingRect { + id: root + + property real animPerc: UPower.displayDevice.percentage + + color: Colours.palette.m3secondaryContainer + radius: Tokens.rounding.large + + implicitWidth: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) || Config.dashboard.performance.showStorage || Config.dashboard.performance.showMemory ? Tokens.sizes.dashboard.perfBattWidth : Tokens.sizes.dashboard.perfBattWidthSingle + implicitHeight: Tokens.sizes.dashboard.perfBattHeight + + Behavior on animPerc { + Anim {} + } + + Contents { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + + accentColour: Colours.palette.m3primary + textColour: Colours.palette.m3onSurface + subTextColour: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitHeight: parent.height * root.animPerc + + color: Colours.palette.m3secondary + radius: Tokens.rounding.extraSmall + clip: true + + Contents { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: layout.anchors.margins + height: layout.height + + accentColour: Colours.palette.m3primaryContainer + textColour: Colours.palette.m3onSecondary + subTextColour: Colours.palette.m3secondaryContainer + } + } + + component Contents: ColumnLayout { + id: contents + + required property color accentColour + required property color textColour + required property color subTextColour + readonly property bool charging: [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) + + spacing: 0 + + MaterialIcon { + Layout.leftMargin: -Tokens.padding.extraSmall + text: "battery_full" + color: contents.accentColour + fontStyle: Tokens.font.icon.large + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery") + color: contents.textColour + font: Tokens.font.body.medium + } + + Item { + Layout.fillHeight: true + } + + StyledText { + Layout.alignment: Qt.AlignRight + text: { + if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) + return qsTr("Full"); + + if (contents.charging) + return qsTr("Charging"); + + const s = UPower.displayDevice.timeToEmpty; + if (s === 0) + return qsTr("..."); + + const hr = Math.floor(s / 3600); + const min = Math.floor((s % 3600) / 60); + if (hr > 0) + return `${hr}h ${min}m`; + + return `${min}m`; + } + color: contents.subTextColour + font: Tokens.font.body.small + animate: true + } + + RowLayout { + Layout.topMargin: -Tokens.padding.extraSmall + Layout.bottomMargin: -Tokens.padding.small + Layout.rightMargin: -Tokens.padding.extraSmall + Layout.alignment: Qt.AlignRight + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + text: "bolt" + color: contents.accentColour + fontStyle: Tokens.font.icon.large + fill: 1 + + scale: contents.charging ? 1 : 0 + opacity: contents.charging ? 1 : 0 + + Behavior on scale { + Anim { + type: Anim.FastSpatial + } + } + + Behavior on opacity { + Anim { + type: Anim.FastEffects + } + } + } + + StyledText { + text: `${Math.round(UPower.displayDevice.percentage * 100)}%` + color: contents.accentColour + font: Tokens.font.headline.medium + } + } + } +} diff --git a/modules/dashboard/performance/HeroCard.qml b/modules/dashboard/performance/HeroCard.qml new file mode 100644 index 00000000..866c6c49 --- /dev/null +++ b/modules/dashboard/performance/HeroCard.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Layouts +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + required property string icon + required property string label + required property string subLabel + required property color accent + required property real usage + required property real temperature + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraLarge + + implicitWidth: Tokens.sizes.dashboard.perfHeroCardWidth + implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.large, usageShape.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2 + + CircularProgress { + id: tempProg + + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Tokens.padding.large + + fgColour: root.accent + + spacing: Tokens.spacing.extraSmall + strokeWidth: Tokens.padding.extraSmall + implicitSize: Math.max(icon.implicitWidth, icon.implicitHeight) + Tokens.padding.medium * 2 + value: root.usage + + Behavior on clampedVal { + Anim {} + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + text: root.icon + color: root.accent + fontStyle: Tokens.font.icon.medium + } + } + + ColumnLayout { + anchors.left: tempProg.right + anchors.right: parent.right + anchors.verticalCenter: tempProg.verticalCenter + anchors.margins: Tokens.spacing.large + spacing: Tokens.spacing.extraSmall + + StyledText { + text: root.label + font: Tokens.font.title.medium + color: root.accent + } + + StyledText { + Layout.fillWidth: true + text: root.subLabel + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } + } + + ColumnLayout { + id: detailsRow + + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.largeIncreased + spacing: Tokens.spacing.extraSmall + + RowLayout { + Layout.leftMargin: -Tokens.padding.extraSmall + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + Layout.topMargin: Math.round(fontInfo.pointSize * 0.08) + text: root.temperature > 90 ? "thermometer_alert" : "thermometer" + color: root.temperature > 90 ? Colours.palette.m3error : root.accent + fontStyle: Tokens.font.icon.medium + fill: 1 + } + + StyledText { + text: `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? root.temperature * 1.8 + 32 : root.temperature)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}` + font: Tokens.font.body.builders.medium.build() + } + } + + StyledProgressBar { + value: root.temperature / 100 + implicitHeight: Tokens.padding.small + fgColour: root.accent + indeterminate: isNaN(root.usage) || isNaN(root.temperature) + } + } + + MaterialShape { + id: usageShape + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.medium + + implicitSize: Tokens.sizes.dashboard.perfUsageShapeSize + color: Colours.palette.m3secondaryContainer + shape: { + if (root.usage >= 0.8) + return MaterialShape.SoftBurst; + if (root.usage >= 0.4) + return MaterialShape.Sunny; + return MaterialShape.Cookie4Sided; + } + + Behavior on color { + CAnim {} + } + + StyledText { + id: usageLabel + + anchors.bottom: parent.top + anchors.horizontalCenter: parent.horizontalCenter + + text: qsTr("Usage") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + } + + StyledText { + anchors.centerIn: parent + text: isNaN(root.usage) ? "...%" : Math.round(root.usage * 100) + "%" + color: root.accent + font: Tokens.font.headline.builders.small.width(50).build() + } + } +} diff --git a/modules/dashboard/performance/MemoryCard.qml b/modules/dashboard/performance/MemoryCard.qml new file mode 100644 index 00000000..5caf4c1e --- /dev/null +++ b/modules/dashboard/performance/MemoryCard.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + readonly property color accent: Colours.palette.m3tertiary + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.medium + + implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ServiceRef { + service: Memory + } + + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Tokens.spacing.extraSmall + + RowLayout { + Layout.leftMargin: -Tokens.padding.extraSmall + spacing: Tokens.spacing.small + + MaterialIcon { + text: "memory_alt" + fill: 1 + color: root.accent + fontStyle: Tokens.font.icon.builders.medium.weight(Font.DemiBold).build() // DemiBold to fix fill issues + } + + StyledText { + text: qsTr("Memory") + font: Tokens.font.title.medium + } + } + + CircularProgress { + Layout.topMargin: Tokens.spacing.large + Layout.alignment: Qt.AlignHCenter + implicitSize: usageColumn.implicitHeight + thickness + Tokens.padding.largeIncreased * 2 + startAngle: -225 + sweepAngle: 270 + + fgColour: root.accent + value: Memory.percentage + + Behavior on clampedVal { + Anim {} + } + + ColumnLayout { + id: usageColumn + + anchors.centerIn: parent + anchors.verticalCenterOffset: Tokens.padding.extraSmall + spacing: 0 + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Math.round(Memory.percentage * 100) + "%" + font: Tokens.font.title.builders.large.width(90).build() + color: root.accent + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Used") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + const fmt = UsageFmt.formatKib(Memory.used, Memory.total); + return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`; + } + font: Tokens.font.body.medium + } + } +} diff --git a/modules/dashboard/performance/NetworkCard.qml b/modules/dashboard/performance/NetworkCard.qml new file mode 100644 index 00000000..4eccebe7 --- /dev/null +++ b/modules/dashboard/performance/NetworkCard.qml @@ -0,0 +1,197 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import Caelestia.Internal +import qs.components +import qs.components.misc +import qs.services + +StyledRect { + id: root + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraLarge + + implicitWidth: Tokens.sizes.dashboard.perfNetworkCardWidth + implicitHeight: Tokens.sizes.dashboard.perfNetworkCardHeight + + Ref { + service: NetworkUsage + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + anchors.bottomMargin: Tokens.padding.medium + spacing: 0 + + RowLayout { + spacing: Tokens.spacing.small + + MaterialIcon { + text: "swap_vert" + color: Colours.palette.m3primary + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Network") + font: Tokens.font.title.medium + } + } + + // Sparkline graph + Item { + Layout.topMargin: Tokens.spacing.medium + Layout.bottomMargin: Tokens.spacing.small + Layout.fillWidth: true + Layout.fillHeight: true + + SparklineItem { + id: sparkline + + property real targetMax: 1024 + property real smoothMax: targetMax + + anchors.fill: parent + line1: NetworkUsage.uploadBuffer // qmllint disable missing-type + line1Color: Colours.palette.m3secondary + line1FillAlpha: 0.15 + line2: NetworkUsage.downloadBuffer // qmllint disable missing-type + line2Color: Colours.palette.m3tertiary + line2FillAlpha: 0.2 + maxValue: smoothMax + historyLength: NetworkUsage.historyLength + + Connections { + function onValuesChanged(): void { + sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); + slideAnim.restart(); + } + + target: NetworkUsage.downloadBuffer + } + + NumberAnimation { + id: slideAnim + + target: sparkline + property: "slideProgress" + from: 0 + to: 1 + easing.type: Easing.Linear + duration: GlobalConfig.dashboard.resourceUpdateInterval + } + + Behavior on smoothMax { + Anim {} + } + } + + // "Collecting data" placeholder + StyledText { + anchors.centerIn: parent + text: qsTr("Collecting data...") + font: Tokens.font.body.small + color: Colours.palette.m3outline + visible: NetworkUsage.downloadBuffer.count < 2 + } + } + + // Download row + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: "download" + color: Colours.palette.m3tertiary + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Download") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() + color: Colours.palette.m3tertiary + } + } + + // Upload row + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: "upload" + color: Colours.palette.m3secondary + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Upload") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font: Tokens.font.body.builders.medium.weight(Font.Medium).build() + color: Colours.palette.m3secondary + } + } + + // Session totals + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: "history" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.medium + } + + StyledText { + text: qsTr("Total") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); + const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); + return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; + } + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } +} diff --git a/modules/dashboard/performance/StorageCard.qml b/modules/dashboard/performance/StorageCard.qml new file mode 100644 index 00000000..f1d5723a --- /dev/null +++ b/modules/dashboard/performance/StorageCard.qml @@ -0,0 +1,135 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import Caelestia.Services +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + readonly property color accent: Colours.palette.m3secondary + readonly property real percentage: Storage.primaryDisk?.perc ?? 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraExtraLarge + + implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ServiceRef { + service: Storage + } + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.extraLarge + spacing: 0 + + RowLayout { + id: row + + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.large + + CircularProgress { + fgColour: root.accent + value: root.percentage + implicitSize: usageColumn.implicitHeight + thickness + Tokens.padding.large * 2 + startAngle: -225 + sweepAngle: 270 + + Behavior on clampedVal { + Anim {} + } + + ColumnLayout { + id: usageColumn + + anchors.centerIn: parent + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "hard_drive" + color: root.accent + fontStyle: Tokens.font.icon.medium + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Math.round(root.percentage * 100) + "%" + font: Tokens.font.title.builders.large.width(90).build() + color: root.accent + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Used") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } + + ColumnLayout { + Layout.minimumWidth: Tokens.sizes.dashboard.perfStorageTextWidth + spacing: Tokens.spacing.extraSmall + + StyledText { + text: qsTr("Storage") + font: Tokens.font.title.medium + } + + StyledText { + text: { + if (!Storage.primaryDisk) + return qsTr("No disks detected"); + + const fmt = UsageFmt.formatKib(Storage.primaryDisk.used, Storage.primaryDisk.total); + return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`; + } + font: Tokens.font.body.large + color: root.accent + } + } + } + + SplitButton { + Layout.alignment: Qt.AlignHCenter + + type: SplitButton.Tonal + disabled: !Storage.disks.length + fallbackIcon: "storage" + fallbackText: qsTr("No disks") + menuOnTop: true + minLeftWidth: row.implicitWidth * 0.6 + + menuItems: disks.instances + active: menuItems.find(m => m.modelData === Storage.primaryDisk) ?? menuItems[0] ?? null + menu.onItemSelected: item => Storage.manualPrimaryDisk = (item as DiskItem).modelData + + Variants { + id: disks + + model: Storage.disks + + DiskItem {} + } + } + } + + component DiskItem: MenuItem { + required property var modelData + + icon: modelData === Storage.primaryDisk ? "check" : "" + text: modelData.mount + activeIcon: "storage" + } +} diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 677c84a0..cc1bccd5 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -63,7 +63,7 @@ StyledWindow { name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: fsTransitionProg > 0 && contentItem.Config.general.showOverFullscreen ? WlrLayer.Overlay : WlrLayer.Top - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: hasFullscreen ? emptyRegion : regions @@ -121,7 +121,9 @@ StyledWindow { color: Colours.palette.m3scrim Behavior on opacity { - Anim {} + Anim { + type: Anim.SlowEffects + } } } @@ -226,9 +228,7 @@ StyledWindow { implicitWidth: panels.popouts.width * (1 + extraWidth) Behavior on extraWidth { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } @@ -312,7 +312,7 @@ StyledWindow { y: panel.y + root.borderThickness implicitWidth: panel.width implicitHeight: panel.height - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge deformScale: (deformAmount * Config.appearance.deformScale) / 10000 } } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 52e20d26..eaeb4538 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -68,6 +68,7 @@ Item { sidebarPanel: sidebar osdPanel: osdWrapper sessionPanel: sessionWrapper + utilitiesPanel: utilities anchors.top: parent.top anchors.right: parent.right @@ -138,7 +139,7 @@ Item { anchors.bottom: sidebar.visible ? parent.bottom : utilities.top anchors.right: sidebar.left - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium } Sidebar.Wrapper { @@ -149,5 +150,6 @@ Item { anchors.top: notifications.bottom anchors.bottom: utilities.top anchors.right: parent.right + anchors.topMargin: -notifications.anchors.topMargin } } diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index a2109c04..6ef55df4 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -32,7 +32,7 @@ StyledListView { highlightFollowsCurrentItem: false highlight: StyledRect { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.palette.m3onSurface opacity: 0.08 @@ -41,9 +41,7 @@ StyledListView { implicitHeight: root.currentItem?.implicitHeight ?? 0 Behavior on y { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } @@ -167,7 +165,8 @@ StyledListView { enabled: !root.state Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" from: 0 to: 1 } @@ -177,7 +176,8 @@ StyledListView { enabled: !root.state Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" from: 1 to: 0 } @@ -188,7 +188,8 @@ StyledListView { property: "y" } Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } } @@ -199,7 +200,8 @@ StyledListView { type: Anim.StandardSmall } Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } } @@ -209,7 +211,8 @@ StyledListView { property: "y" } Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } } diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 69be0c3a..1c990388 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia import Caelestia.Config import qs.components import qs.components.controls @@ -15,10 +16,10 @@ Item { required property real maxHeight readonly property int padding: Tokens.padding.large - readonly property int rounding: Tokens.rounding.large + readonly property int rounding: Tokens.rounding.extraLarge implicitWidth: listWrapper.width + padding * 2 - implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 + implicitHeight: searchWrapper.height + listWrapper.height + padding + searchWrapper.anchors.bottomMargin Item { id: listWrapper @@ -53,6 +54,7 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: root.padding + anchors.bottomMargin: CUtils.clamp(root.padding - Config.border.thickness, 0, root.padding) implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight) @@ -75,8 +77,8 @@ Item { anchors.leftMargin: Tokens.spacing.small anchors.rightMargin: Tokens.spacing.small - topPadding: Tokens.padding.larger - bottomPadding: Tokens.padding.larger + topPadding: Tokens.padding.medium + bottomPadding: Tokens.padding.medium placeholderText: qsTr("Type \"%1\" for commands").arg(GlobalConfig.launcher.actionPrefix) diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index db7abdf9..f42087ed 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -60,7 +60,7 @@ Item { property: "opacity" from: 1 to: 0 - type: Anim.StandardSmall + type: Anim.DefaultEffects } PropertyAction {} Anim { @@ -68,7 +68,7 @@ Item { property: "opacity" from: 0 to: 1 - type: Anim.StandardSmall + type: Anim.DefaultEffects } } } @@ -110,7 +110,7 @@ Item { opacity: root.currentList?.count === 0 ? 1 : 0 scale: root.currentList?.count === 0 ? 1 : 0.5 - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium padding: Tokens.padding.large anchors.horizontalCenter: parent.horizontalCenter @@ -119,7 +119,7 @@ Item { MaterialIcon { text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge anchors.verticalCenter: parent.verticalCenter } @@ -130,19 +130,20 @@ Item { StyledText { text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.larger - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } StyledText { text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -153,18 +154,12 @@ Item { Behavior on implicitWidth { enabled: root.visibilities.launcher - Anim { - duration: Tokens.anim.durations.large - easing: Tokens.anim.emphasizedDecel - } + Anim {} } Behavior on implicitHeight { enabled: root.visibilities.launcher - Anim { - duration: Tokens.anim.durations.large - easing: Tokens.anim.emphasizedDecel - } + Anim {} } } diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index 3663f4bc..8f16e2a1 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -15,7 +15,7 @@ PathView { required property var panels required property var content - readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 + readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.medium * 2 readonly property int numItems: { const screen = (QsWindow.window as QsWindow)?.screen; diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index d5630b23..290c8dc6 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -16,7 +16,7 @@ Item { readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled readonly property real maxHeight: { - let max = screen.height - Config.border.thickness * 2 - Tokens.spacing.large; + let max = screen.height - Config.border.thickness * 2 + Tokens.padding.large; if (visibilities.dashboard) max -= panels.dashboard.nonAnimHeight; return max; @@ -40,9 +40,7 @@ Item { Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index 0f9fb5dd..b7bbebbb 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -15,28 +15,28 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small MaterialIcon { id: icon - text: root.modelData?.icon ?? "" - font.pointSize: Tokens.font.size.extraLarge - anchors.verticalCenter: parent.verticalCenter + text: root.modelData?.icon ?? "" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.builders.large.scale(1.3).build() } Item { anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width @@ -46,18 +46,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { id: desc text: root.modelData?.desc ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Tokens.rounding.normal * 2 + width: root.width - icon.width - Tokens.rounding.extraLargeIncreased anchors.top: name.bottom } diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 80739c8f..ba6b0aab 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -19,7 +19,7 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: { Apps.launch(root.modelData); root.visibilities.launcher = false; @@ -28,9 +28,9 @@ Item { Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small IconImage { id: icon @@ -44,7 +44,7 @@ Item { Item { anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width - favouriteIcon.width @@ -54,18 +54,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { id: comment text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.normal * 2 + width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.extraLargeIncreased anchors.top: name.bottom } diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index f746f96c..38097123 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -28,7 +28,7 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.onClicked() } @@ -36,13 +36,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.larger + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium MaterialIcon { text: "function" - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge Layout.alignment: Qt.AlignVCenter } @@ -66,11 +66,11 @@ Item { StyledRect { color: Colours.palette.m3tertiary - radius: Tokens.rounding.normal + radius: Tokens.rounding.large clip: true - implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small * 2 + implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small Layout.alignment: Qt.AlignVCenter @@ -94,12 +94,14 @@ Item { text: qsTr("Open in calculator") color: Colours.palette.m3onTertiary - font.pointSize: Tokens.font.size.normal + font: Tokens.font.label.medium opacity: stateLayer.containsMouse ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -108,11 +110,11 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.medium text: "open_in_new" color: Colours.palette.m3onTertiary - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } Behavior on implicitWidth { diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index ab0784bd..a2f466cc 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -16,15 +16,15 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small StyledRect { id: preview @@ -61,20 +61,20 @@ Item { Column { anchors.left: preview.right - anchors.leftMargin: Tokens.spacing.normal + anchors.leftMargin: Tokens.spacing.medium anchors.verticalCenter: parent.verticalCenter - width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) + width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.medium : 0) spacing: 0 StyledText { text: root.modelData?.flavour ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -95,7 +95,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index b13d5d8e..fa3fb259 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -16,41 +16,41 @@ Item { anchors.right: parent?.right StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Tokens.padding.larger - anchors.rightMargin: Tokens.padding.larger - anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + anchors.margins: Tokens.padding.small MaterialIcon { id: icon text: root.modelData?.icon ?? "" - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge anchors.verticalCenter: parent.verticalCenter } Column { anchors.left: icon.right - anchors.leftMargin: Tokens.spacing.larger + anchors.leftMargin: Tokens.spacing.large anchors.verticalCenter: icon.verticalCenter - width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) + width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.medium : 0) spacing: 0 StyledText { text: root.modelData?.name ?? "" - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } StyledText { text: root.modelData?.description ?? "" - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -71,7 +71,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index d09a894c..9f87cba7 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -22,11 +22,11 @@ Item { opacity = Qt.binding(() => PathView.onPath ? 1 : 0); } - implicitWidth: image.width + Tokens.padding.larger * 2 - implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal + implicitWidth: image.width + Tokens.padding.medium * 2 + implicitHeight: image.height + label.height + Tokens.spacing.extraSmall + Tokens.padding.large + Tokens.padding.medium StateLayer { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large onClicked: { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; @@ -40,7 +40,9 @@ Item { level: 4 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -50,7 +52,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter y: Tokens.padding.large color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal + radius: Tokens.rounding.large implicitWidth: Tokens.sizes.launcher.wallpaperWidth implicitHeight: implicitWidth / 16 * 9 @@ -59,8 +61,7 @@ Item { anchors.centerIn: parent text: "image" color: Colours.tPalette.m3outline - font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 600 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build() } CachingImage { @@ -78,15 +79,15 @@ Item { id: label anchors.top: image.bottom - anchors.topMargin: Tokens.spacing.small / 2 + anchors.topMargin: Tokens.spacing.extraSmall anchors.horizontalCenter: parent.horizontalCenter - width: image.width - Tokens.padding.normal * 2 + width: image.width - Tokens.padding.medium * 2 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight renderType: Text.QtRendering text: root.modelData.relativePath - font.pointSize: Tokens.font.size.normal + font: Tokens.font.label.medium } Behavior on scale { @@ -94,6 +95,8 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index a987d4eb..30b3ab59 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -1,13 +1,9 @@ -pragma ComponentBehavior: Bound - +import "center" import QtQuick import QtQuick.Layouts import Caelestia.Config import qs.components -import qs.components.controls -import qs.components.images import qs.services -import qs.utils ColumnLayout { id: root @@ -20,396 +16,37 @@ ColumnLayout { Layout.fillWidth: false Layout.fillHeight: true - spacing: Tokens.spacing.large * 2 + spacing: Tokens.spacing.largeIncreased - RowLayout { + Clock { Layout.alignment: Qt.AlignHCenter - spacing: Tokens.spacing.small - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.hourStr - color: Colours.palette.m3secondary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: ":" - color: Colours.palette.m3primary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.minuteStr - color: Colours.palette.m3secondary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - - Loader { - asynchronous: true - Layout.leftMargin: Tokens.spacing.small - Layout.alignment: Qt.AlignVCenter - - active: GlobalConfig.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: Time.amPmStr - color: Colours.palette.m3primary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * 2 * root.centerScale) - font.family: Tokens.font.family.clock - font.bold: true - } - } + Layout.topMargin: Tokens.padding.large + centerScale: root.centerScale } StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -Tokens.padding.large * 2 - text: Time.format("dddd, d MMMM yyyy") - color: Colours.palette.m3tertiary - font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale) - font.family: Tokens.font.family.mono - font.bold: true + text: Time.format("dddd • d MMM").toUpperCase() + color: Colours.palette.m3onSurface + font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build() } - StyledClippingRect { - Layout.topMargin: Tokens.spacing.large * 2 + ProfilePic { Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth / 2 - implicitHeight: root.centerWidth / 2 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.full - - MaterialIcon { - anchors.centerIn: parent - - text: "person" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Math.floor(root.centerWidth / 4) - visible: pfp.status !== Image.Ready - } - - CachingImage { - id: pfp - - anchors.fill: parent - path: `${Paths.home}/.face` - } + Layout.topMargin: Tokens.spacing.largeIncreased + Layout.bottomMargin: Tokens.spacing.medium + centerWidth: root.centerWidth } - StyledRect { + PasswordInput { Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth * 0.8 - implicitHeight: input.implicitHeight + Tokens.padding.small * 2 - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.full - - focus: true - onActiveFocusChanged: { - if (!activeFocus) - forceActiveFocus(); - } - - Keys.onPressed: event => { - if (root.lock.unlocking) - return; - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) - inputField.placeholder.animate = false; - - root.lock.pam.handleKey(event); - } - - StateLayer { - onClicked: { - parent.forceActiveFocus(); - } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor - } - - RowLayout { - id: input - - anchors.fill: parent - anchors.margins: Tokens.padding.small - spacing: Tokens.spacing.normal - - Item { - implicitWidth: implicitHeight - implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small * 2 - - MaterialIcon { - id: fprintIcon - - anchors.centerIn: parent - animate: true - text: { - if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) - return "fingerprint_off"; - if (root.lock.pam.fprint.active) - return "fingerprint"; - return "lock"; - } - color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface - opacity: root.lock.pam.passwd.active ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - CircularIndicator { - anchors.fill: parent - running: root.lock.pam.passwd.active - } - } - - InputField { - id: inputField - - pam: root.lock.pam - } - - StyledRect { - implicitWidth: implicitHeight - implicitHeight: enterIcon.implicitHeight + Tokens.padding.small * 2 - - color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.full - - StateLayer { - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - onClicked: root.lock.pam.passwd.start() - } - - MaterialIcon { - id: enterIcon - - anchors.centerIn: parent - text: "arrow_forward" - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - font.weight: 500 - } - } - } + centerWidth: root.centerWidth + lock: root.lock } - Item { + StateMessage { Layout.fillWidth: true - Layout.topMargin: -Tokens.spacing.large - - implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) - - Behavior on implicitHeight { - Anim {} - } - - StyledText { - id: stateMessage - - readonly property string msg: { - if (Hypr.kbLayout !== Hypr.defaultKbLayout) { - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.capsLock) - return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.numLock) - return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); - } - - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON."); - if (Hypr.capsLock) - return qsTr("Caps lock is ON."); - if (Hypr.numLock) - return qsTr("Num lock is ON."); - - return ""; - } - - property bool shouldBeVisible - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - } else { - text = msg; - } - shouldBeVisible = true; - } else { - shouldBeVisible = false; - } - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: shouldBeVisible && !message.msg ? 1 : 0.7 - opacity: shouldBeVisible && !message.msg ? 1 : 0 - color: Colours.palette.m3onSurfaceVariant - animateProp: "opacity" - - font.family: Tokens.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - lineHeight: 1.2 - - Behavior on scale { - Anim {} - } - - Behavior on opacity { - Anim {} - } - } - - StyledText { - id: message - - readonly property Pam pam: root.lock.pam - readonly property string msg: { - if (pam.fprintState === "error") - return qsTr("FP ERROR: %1").arg(pam.fprint.message); - if (pam.state === "error") - return qsTr("PW ERROR: %1").arg(pam.passwd.message); - - if (pam.lockMessage) - return pam.lockMessage; - - if (pam.state === "max" && pam.fprintState === "max") - return qsTr("Maximum password and fingerprint attempts reached."); - if (pam.state === "max") { - if (pam.fprint.available) - return qsTr("Maximum password attempts reached. Please use fingerprint."); - return qsTr("Maximum password attempts reached."); - } - if (pam.fprintState === "max") - return qsTr("Maximum fingerprint attempts reached. Please use password."); - - if (pam.state === "fail") { - if (pam.fprint.available) - return qsTr("Incorrect password. Please try again or use fingerprint."); - return qsTr("Incorrect password. Please try again."); - } - if (pam.fprintState === "fail") - return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); - - return ""; - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: 0.7 - opacity: 0 - color: Colours.palette.m3error - - font.pointSize: Tokens.font.size.small - font.family: Tokens.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - - exitAnim.stop(); - if (scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } else { - text = msg; - exitAnim.stop(); - appearAnim.restart(); - } - } else { - appearAnim.stop(); - flashAnim.stop(); - exitAnim.start(); - } - } - - Connections { - function onFlashMsg(): void { - exitAnim.stop(); - if (message.scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } - - target: root.lock.pam - } - - Anim { - id: appearAnim - - target: message - properties: "scale,opacity" - to: 1 - onFinished: flashAnim.restart() - } - - SequentialAnimation { - id: flashAnim - - loops: 2 - - FlashAnim { - to: 0.3 - } - FlashAnim { - to: 1 - } - } - - ParallelAnimation { - id: exitAnim - - Anim { - target: message - property: "scale" - to: 0.7 - type: Anim.StandardLarge - } - Anim { - target: message - property: "opacity" - to: 0 - type: Anim.StandardLarge - } - } - } - } - - component FlashAnim: NumberAnimation { - target: message - property: "opacity" - duration: Tokens.anim.durations.small - easing.type: Easing.Linear + pam: root.lock.pam } } diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index 0ff7daad..0d47502a 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -9,50 +9,26 @@ RowLayout { required property var lock - spacing: Tokens.spacing.large * 2 + spacing: Tokens.spacing.largeIncreased * 2 ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium - StyledRect { + WeatherInfo { Layout.fillWidth: true - implicitHeight: weather.implicitHeight - - topLeftRadius: Tokens.rounding.large - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - WeatherInfo { - id: weather - - rootHeight: root.height - } + rootHeight: root.height } - StyledRect { + Fetch { + Layout.fillWidth: true + rootHeight: root.height + } + + Media { Layout.fillWidth: true Layout.fillHeight: true - - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Fetch {} - } - - StyledClippingRect { - Layout.fillWidth: true - implicitHeight: media.implicitHeight - - bottomLeftRadius: Tokens.rounding.large - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Media { - id: media - - lock: root.lock - } + lock: root.lock } } @@ -62,27 +38,18 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium - StyledRect { + Resources { Layout.fillWidth: true - implicitHeight: resources.implicitHeight - - topRightRadius: Tokens.rounding.large - radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Resources { - id: resources - } } StyledRect { Layout.fillWidth: true Layout.fillHeight: true - bottomRightRadius: Tokens.rounding.large - radius: Tokens.rounding.small + bottomRightRadius: Tokens.rounding.extraLarge + radius: Tokens.rounding.medium color: Colours.tPalette.m3surfaceContainer NotifDock { diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index 0afbe455..0094b5f0 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -3,140 +3,147 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower +import Caelestia import Caelestia.Config import qs.components import qs.components.effects import qs.services import qs.utils -ColumnLayout { +StyledRect { id: root - anchors.fill: parent - anchors.margins: Tokens.padding.large * 2 - anchors.topMargin: Tokens.padding.large + required property real rootHeight + readonly property int cBoxSize: Tokens.font.body.medium.pointSize * 2 - spacing: Tokens.spacing.small + implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.margins + radius: Tokens.rounding.medium + color: Colours.tPalette.m3surfaceContainer - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - spacing: Tokens.spacing.normal + ColumnLayout { + id: layout - StyledRect { - implicitWidth: prompt.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: prompt.implicitHeight + Tokens.padding.normal * 2 + anchors.fill: parent + anchors.margins: Tokens.padding.extraLarge + anchors.topMargin: Tokens.padding.extraLarge + anchors.bottomMargin: Tokens.padding.extraLarge - color: Colours.palette.m3primary - radius: Tokens.rounding.small + spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: false + spacing: Tokens.spacing.medium + + StyledRect { + implicitWidth: prompt.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: prompt.implicitHeight + Tokens.padding.small * 2 + + color: Colours.palette.m3primary + radius: Tokens.rounding.medium + + MonoText { + id: prompt + + anchors.centerIn: parent + text: ">" + color: Colours.palette.m3onPrimary + } + } MonoText { - id: prompt + Layout.fillWidth: true + text: "caelestiafetch.sh" + elide: Text.ElideRight + } - anchors.centerIn: parent - text: ">" - font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - color: Colours.palette.m3onPrimary + WrappedLoader { + Layout.fillHeight: true + Layout.preferredWidth: height + Layout.preferredHeight: 0 + active: !iconLoader.active + + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } } - MonoText { + RowLayout { Layout.fillWidth: true - text: "caelestiafetch.sh" - font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - elide: Text.ElideRight + Layout.fillHeight: true + spacing: Tokens.spacing.extraLarge + + WrappedLoader { + id: iconLoader + + Layout.fillHeight: true + active: root.width > Tokens.sizes.lock.largeLogoWidth + + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: Tokens.padding.medium + Layout.bottomMargin: iconLoader.active || colourRowLoader.active ? Tokens.padding.medium : 0 + spacing: Tokens.spacing.medium + + Repeater { + model: { + const items = []; + const hasBatt = UPower.displayDevice.isLaptopBattery; + const rHeight = root.rootHeight; + + if (!hasBatt && rHeight > Tokens.sizes.lock.fetch4LinesHeight) + items.push(`OS : ${SysInfo.osPrettyName || SysInfo.osName}`); + + if (rHeight > (hasBatt ? Tokens.sizes.lock.fetch4LinesHeight : Tokens.sizes.lock.fetch3LinesHeight)) + items.push(`WM : ${SysInfo.wm}`); + + if (!hasBatt || rHeight > Tokens.sizes.lock.fetch3LinesHeight) + items.push(`USER: ${SysInfo.user}`); + + items.push(`UP : ${SysInfo.uptime}`); + + if (hasBatt) + items.push(`BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%`); + + return items; + } + + MonoText { + required property string modelData + + Layout.fillWidth: true + text: modelData + elide: Text.ElideRight + } + } + } } WrappedLoader { - Layout.fillHeight: true - active: !iconLoader.active + id: colourRowLoader - sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon - } - } + Layout.topMargin: iconLoader.active ? Tokens.spacing.small : 0 + Layout.alignment: Qt.AlignHCenter + active: root.rootHeight > Tokens.sizes.lock.showColourBoxRowHeight - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - spacing: height * 0.15 + sourceComponent: RowLayout { + id: coloursRow - WrappedLoader { - id: iconLoader + spacing: Tokens.spacing.largeIncreased - Layout.fillHeight: true - active: root.width > 320 + Repeater { + model: CUtils.clamp(Math.floor((layout.width + coloursRow.spacing) / (root.cBoxSize + coloursRow.spacing)), 0, 8) - sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon - } + StyledRect { + required property int index - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: Tokens.padding.normal - Layout.bottomMargin: Tokens.padding.normal - Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 - spacing: Tokens.spacing.normal - - WrappedLoader { - Layout.fillWidth: true - active: !batLoader.active && root.height > 200 - - sourceComponent: FetchText { - text: `OS : ${SysInfo.osPrettyName || SysInfo.osName}` - } - } - - WrappedLoader { - Layout.fillWidth: true - active: root.height > (batLoader.active ? 200 : 110) - - sourceComponent: FetchText { - text: `WM : ${SysInfo.wm}` - } - } - - WrappedLoader { - Layout.fillWidth: true - active: !batLoader.active || root.height > 110 - - sourceComponent: FetchText { - text: `USER: ${SysInfo.user}` - } - } - - FetchText { - text: `UP : ${SysInfo.uptime}` - } - - WrappedLoader { - id: batLoader - - Layout.fillWidth: true - active: UPower.displayDevice.isLaptopBattery - - sourceComponent: FetchText { - text: `BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%` - } - } - } - } - - WrappedLoader { - Layout.alignment: Qt.AlignHCenter - active: root.height > 180 - - sourceComponent: RowLayout { - spacing: Tokens.spacing.large - - Repeater { - model: Math.max(0, Math.min(8, root.width / (Tokens.font.size.larger * 2 + Tokens.spacing.large))) - - StyledRect { - required property int index - - implicitWidth: implicitHeight - implicitHeight: Tokens.font.size.larger * 2 - color: Colours.palette[`term${index}`] - radius: Tokens.rounding.small + implicitWidth: implicitHeight + implicitHeight: root.cBoxSize + color: Colours.palette[`term${index}`] + radius: Tokens.rounding.medium + } } } } @@ -166,13 +173,7 @@ ColumnLayout { visible: active } - component FetchText: MonoText { - Layout.fillWidth: true - font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - elide: Text.ElideRight - } - component MonoText: StyledText { - font.family: Tokens.font.family.mono + font: root.width > Tokens.sizes.lock.largeFontWidth ? Tokens.font.mono.medium : Tokens.font.mono.small } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 322773fd..2c97248d 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -36,7 +36,6 @@ WlSessionLockSurface { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size - type: Anim.DefaultSpatial } Anim { target: lockBg @@ -47,7 +46,6 @@ WlSessionLockSurface { target: content property: "scale" to: 0 - type: Anim.DefaultSpatial } Anim { target: content @@ -72,6 +70,7 @@ WlSessionLockSurface { duration: Tokens.anim.durations.small } Anim { + type: Anim.Standard target: lockContent property: "opacity" to: 0 @@ -120,11 +119,13 @@ WlSessionLockSurface { easing: Tokens.anim.standardDecel } Anim { + type: Anim.DefaultEffects target: lockIcon property: "opacity" to: 0 } Anim { + type: Anim.DefaultEffects target: content property: "opacity" to: 1 @@ -133,24 +134,21 @@ WlSessionLockSurface { target: content property: "scale" to: 1 - type: Anim.DefaultSpatial } Anim { target: lockBg property: "radius" - to: lockContent.Tokens.rounding.large * 1.5 + to: lockContent.Tokens.rounding.extraLarge * 1.5 } Anim { target: lockContent property: "implicitWidth" to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio - type: Anim.DefaultSpatial } Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult - type: Anim.DefaultSpatial } } } @@ -207,8 +205,7 @@ WlSessionLockSurface { anchors.centerIn: parent text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 4 - font.bold: true + fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build() rotation: 180 } @@ -216,8 +213,8 @@ WlSessionLockSurface { id: content anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 - height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 + width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased + height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased lock: root opacity: 0 diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index c55333c7..3a8dc794 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -1,23 +1,23 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Components import Caelestia.Config import qs.components -import qs.components.effects +import qs.components.controls +import qs.components.images import qs.services -Item { +StyledClippingRect { id: root required property var lock - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: layout.implicitHeight + implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 + radius: Tokens.rounding.extraLarge + color: Colours.tPalette.m3surfaceContainer - Image { + FadeImage { anchors.fill: parent source: Players.getArtUrl(Players.active) @@ -29,12 +29,14 @@ Item { } layer.enabled: true - layer.effect: OpacityMask { - maskSource: mask - } - opacity: status === Image.Ready ? 1 : 0 + StyledRect { + anchors.fill: parent + color: Colours.palette.m3surface + opacity: 0.7 + } + Behavior on opacity { Anim { type: Anim.StandardExtraLarge @@ -42,160 +44,67 @@ Item { } } - Rectangle { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - gradient: Gradient { - orientation: Gradient.Horizontal - - GradientStop { - position: 0 - color: Qt.rgba(0, 0, 0, 0.5) - } - GradientStop { - position: 0.4 - color: Qt.rgba(0, 0, 0, 0.2) - } - GradientStop { - position: 0.8 - color: Qt.rgba(0, 0, 0, 0) - } - } - } - ColumnLayout { id: layout anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.large - - StyledText { - Layout.topMargin: Tokens.padding.large - Layout.bottomMargin: Tokens.spacing.larger - text: qsTr("Now playing") - color: Colours.palette.m3onSurfaceVariant - font.family: Tokens.font.family.mono - font.weight: 500 - } + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.extraLarge + spacing: Tokens.spacing.extraSmall StyledText { Layout.fillWidth: true animate: true - text: Players.active?.trackArtist ?? qsTr("No media") + text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track") color: Colours.palette.m3primary horizontalAlignment: Text.AlignHCenter - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.mono - font.weight: 600 + font: Tokens.font.title.medium elide: Text.ElideRight } StyledText { Layout.fillWidth: true animate: true - text: Players.active?.trackTitle ?? qsTr("No media") + text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist") + color: Colours.palette.m3onSurfaceVariant horizontalAlignment: Text.AlignHCenter - font.pointSize: Tokens.font.size.larger - font.family: Tokens.font.family.mono + font: Tokens.font.body.small elide: Text.ElideRight } - RowLayout { + ButtonRow { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Tokens.spacing.large * 1.2 - Layout.bottomMargin: Tokens.padding.large + Layout.topMargin: Tokens.spacing.medium - spacing: Tokens.spacing.large + spacing: Tokens.spacing.extraSmall - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_previous" - onClicked: { - if (Players.active?.canGoPrevious) - Players.active.previous(); - } + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoPrevious + onClicked: Players.active?.previous() } - PlayerControl { - animate: true - icon: active ? "pause" : "play_arrow" - colour: "Primary" - level: active ? 2 : 1 - active: Players.active?.isPlaying ?? false - onClicked: { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } + IconButton { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + isRound: true + shapeMorph: true + checked: Players.active?.isPlaying ?? false + disabled: !Players.active?.canTogglePlaying + onClicked: Players.active?.togglePlaying() + implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2 } - PlayerControl { + IconButton { + type: IconButton.Tonal icon: "skip_next" - onClicked: { - if (Players.active?.canGoNext) - Players.active.next(); - } - } - } - } - - component PlayerControl: StyledRect { - id: control - - property alias animate: controlIcon.animate - property alias icon: controlIcon.text - property bool active - property string colour: "Secondary" - property int level: 1 - - signal clicked - - Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) - implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 - implicitHeight: controlIcon.implicitHeight + Tokens.padding.normal * 2 - - color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] - radius: active || controlState.pressed ? Tokens.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale) - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level - } - - StateLayer { - id: controlState - - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - onClicked: control.clicked() - } - - MaterialIcon { - id: controlIcon - - anchors.centerIn: parent - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - font.pointSize: Tokens.font.size.large - fill: control.active ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial - } - } - - Behavior on radius { - Anim { - type: Anim.FastSpatial + isRound: true + shapeMorph: true + disabled: !Players.active?.canGoNext + onClicked: Players.active?.next() } } } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 2254dfe0..ed5a396d 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -19,14 +19,13 @@ ColumnLayout { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.mono.builders.small.weight(Font.Medium).build() elide: Text.ElideRight } @@ -36,7 +35,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: "transparent" Loader { @@ -46,7 +45,7 @@ ColumnLayout { opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Tokens.spacing.large + spacing: Tokens.spacing.largeIncreased Image { asynchronous: true @@ -65,9 +64,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") color: Colours.palette.m3outlineVariant - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.mono.builders.large.weight(Font.Medium).build() } } @@ -95,6 +92,7 @@ ColumnLayout { add: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" from: 0 to: 1 @@ -103,12 +101,12 @@ ColumnLayout { property: "scale" from: 0 to: 1 - type: Anim.DefaultSpatial } } remove: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" to: 0 } @@ -120,23 +118,23 @@ ColumnLayout { move: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,scale" to: 1 } Anim { property: "y" - type: Anim.DefaultSpatial } } displaced: Transition { Anim { + type: Anim.DefaultEffects properties: "opacity,scale" to: 1 } Anim { property: "y" - type: Anim.DefaultSpatial } } } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 971b921a..2337376b 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -46,10 +46,10 @@ StyledRect { anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: content.implicitHeight + Tokens.padding.medium * 2 clip: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) RowLayout { @@ -58,9 +58,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -101,7 +101,7 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -142,21 +142,21 @@ StyledRect { } ColumnLayout { - Layout.topMargin: -Tokens.padding.small - Layout.bottomMargin: -Tokens.padding.small / 2 - (root.expanded ? 0 : spacing) + Layout.topMargin: -Tokens.padding.extraSmall + Layout.bottomMargin: -Tokens.padding.extraSmall / 2 - (root.expanded ? 0 : spacing) Layout.fillWidth: true - spacing: Math.round(Tokens.spacing.small / 2) + spacing: Math.round(Tokens.spacing.extraSmall) RowLayout { Layout.bottomMargin: -parent.spacing Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } @@ -164,12 +164,12 @@ StyledRect { animate: true text: root.notifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Tokens.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.large + implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) radius: Tokens.rounding.full @@ -186,20 +186,20 @@ StyledRect { id: expandBtn anchors.centerIn: parent - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { id: groupCount - Layout.leftMargin: Tokens.padding.small / 2 + Layout.leftMargin: Tokens.padding.extraSmall / 2 animate: true text: root.notifs.length color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } MaterialIcon { - Layout.rightMargin: -Tokens.padding.small / 2 + Layout.rightMargin: -Tokens.padding.extraSmall / 2 animate: true text: root.expanded ? "expand_less" : "expand_more" color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface @@ -207,7 +207,9 @@ StyledRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on Layout.preferredWidth { @@ -228,6 +230,7 @@ StyledRect { running: true Anim { + type: Anim.DefaultEffects target: notif property: "opacity" from: 0 @@ -252,6 +255,7 @@ StyledRect { onFinished: notif.modelData.unlock(notif) Anim { + type: Anim.DefaultEffects target: notif property: "opacity" to: 0 @@ -289,16 +293,16 @@ StyledRect { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } component NotifLine: StyledText { @@ -331,8 +335,7 @@ StyledRect { id: metrics text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ") - font.pointSize: notifLine.font.pointSize - font.family: notifLine.font.family + font: notifLine.font elideWidth: notifLine.width elide: Text.ElideRight } diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 1f38aa00..caa39e5b 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,93 +1,183 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts +import M3Shapes import Caelestia.Config +import Caelestia.Services import qs.components -import qs.components.controls -import qs.components.misc +import qs.components.effects +import qs.components.widgets import qs.services -GridLayout { +StyledRect { id: root - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Tokens.padding.large - - rowSpacing: Tokens.spacing.large - columnSpacing: Tokens.spacing.large - rows: 2 - columns: 2 - - Ref { - service: SystemUsage + readonly property real fontScale: { + const diff = width / 391 - 1; // 391 is the width at 1080 height screen + return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff); } - Resource { - Layout.topMargin: Tokens.padding.large - icon: "memory" - value: SystemUsage.cpuPerc - colour: Colours.palette.m3primary + implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 + radius: Tokens.rounding.extraLarge + color: Colours.tPalette.m3surfaceContainer + + ServiceRef { + service: Cpu } - Resource { - Layout.topMargin: Tokens.padding.large - icon: "thermostat" - value: Math.min(1, SystemUsage.cpuTemp / 90) - colour: Colours.palette.m3secondary + ServiceRef { + service: Memory } - Resource { - Layout.bottomMargin: Tokens.padding.large - icon: "memory_alt" - value: SystemUsage.memPerc - colour: Colours.palette.m3secondary + ServiceRef { + service: Storage } - Resource { - Layout.bottomMargin: Tokens.padding.large - icon: "hard_disk" - value: SystemUsage.storagePerc - colour: Colours.palette.m3tertiary + RowLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.large + + Resource { + id: cpu + + icon: "memory" + value: Math.round(Cpu.percentage * 100) + "%" + fillValue: Cpu.percentage + colour: Colours.palette.m3primary + shapeColour: Colours.palette.m3primaryContainer + fillColour: Qt.alpha(Colours.palette.m3secondary, 0.3) + shape: MaterialShape.Pentagon + + MaterialShape { + x: cpu.mShape.pointAtAngle(45).x - implicitSize / 2 + Tokens.padding.medium + y: cpu.mShape.pointAtAngle(45).y - implicitSize / 2 + + shape: Cpu.temperature > 90 ? MaterialShape.SoftBurst : MaterialShape.Circle + color: Cpu.temperature > 90 ? Colours.palette.m3errorContainer : Colours.palette.m3secondaryContainer + implicitSize: { + const size = Math.round(tempLabel.implicitHeight * 2); + return size % 2 === 0 ? size : size + 1; // Ensure even size so center works properly + } + + Behavior on color { + CAnim {} + } + + StyledText { + id: tempLabel + + anchors.centerIn: parent + anchors.verticalCenterOffset: Math.round(fontInfo.pointSize * 0.04) + + text: { + const temp = Cpu.temperature; + const useF = GlobalConfig.services.useFahrenheitPerformance; + return `${Math.ceil(useF ? temp * 1.8 + 32 : temp)}°${useF ? "F" : "C"}`; + } + color: Cpu.temperature > 90 ? Colours.palette.m3onErrorContainer : Colours.palette.m3secondary + font: Tokens.font.title.builders.medium.scale(cpu.width / 112).width(50).build() + } + } + } + + Resource { + icon: "memory_alt" + value: Math.round(Memory.percentage * 100) + "%" + fillValue: Memory.percentage + colour: Colours.palette.m3tertiary + shapeColour: Colours.palette.m3onTertiary + fillColour: Qt.alpha(Colours.palette.m3tertiary, 0.3) + shape: MaterialShape.Slanted + } + + Resource { + icon: "hard_disk" + value: Math.round(Storage.percentage * 100) + "%" + fillValue: Storage.percentage + colour: Colours.palette.m3secondary + shapeColour: Colours.palette.m3secondaryContainer + fillColour: Qt.alpha(Colours.palette.m3secondary, 0.4) + shape: MaterialShape.Gem + } } - component Resource: StyledRect { + component Resource: Item { id: res required property string icon - required property real value + required property string value required property color colour + required property color shapeColour + property color fillColour + property real fillValue: -1 + property alias shape: shape.shape + readonly property alias mShape: shape Layout.fillWidth: true implicitHeight: width - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Tokens.rounding.large - - CircularProgress { - id: circ - - anchors.fill: parent - value: res.value - padding: Tokens.padding.large * 3 - fgColour: res.colour - bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) - strokeWidth: width < 200 ? Tokens.padding.smaller : Tokens.padding.normal + Behavior on shapeColour { + CAnim {} } - MaterialIcon { - id: icon + MaterialShape { + id: shape - anchors.centerIn: parent - text: res.icon - color: res.colour - font.pointSize: (circ.arcRadius * 0.7) || 1 - font.weight: 600 + implicitSize: res.width + color: Qt.alpha(res.shapeColour, 1) + opacity: res.shapeColour.a + layer.enabled: true } - Behavior on value { - Anim { - type: Anim.StandardLarge + Loader { + id: fillLoader + + anchors.fill: shape + active: res.fillValue >= 0 + asynchronous: true + + layer.enabled: active + layer.effect: Mask { + maskSource: shape } + + sourceComponent: Item { + WavyTopRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + implicitHeight: shape.implicitSize * res.fillValue + color: res.fillColour + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: -Tokens.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: res.icon + color: Colours.palette.m3secondary + fontStyle: Tokens.font.icon.builders.medium.scale(root.fontScale).build() + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: res.value + color: res.colour + font: Tokens.font.headline.builders.large.scale(root.fontScale).width(50).build() + } + } + + Behavior on fillValue { + Anim {} } } } diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index d96a54df..74ae255c 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -1,172 +1,23 @@ -pragma ComponentBehavior: Bound - +import "weather" import QtQuick -import QtQuick.Layouts import Caelestia.Config import qs.components import qs.services -ColumnLayout { +StyledRect { id: root required property int rootHeight + readonly property bool showForecast: rootHeight >= Tokens.sizes.lock.showForecastHeight - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Tokens.padding.large * 2 - - spacing: Tokens.spacing.small - - Loader { - asynchronous: true - Layout.topMargin: Tokens.padding.large * 2 - Layout.bottomMargin: -Tokens.padding.large - Layout.alignment: Qt.AlignHCenter - - active: root.rootHeight > 610 - visible: active - - sourceComponent: StyledText { - text: qsTr("Weather") - color: Colours.palette.m3primary - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.spacing.large - - MaterialIcon { - animate: true - text: Weather.icon - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.extraLarge * 2.5 - } - - ColumnLayout { - spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true - - animate: true - text: Weather.description - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.large - font.weight: 500 - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - - animate: true - text: qsTr("Humidity: %1%").arg(Weather.humidity) - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - } - - Loader { - asynchronous: true - Layout.rightMargin: Tokens.padding.smaller - active: root.width > 400 - visible: active - - sourceComponent: ColumnLayout { - spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true - - animate: true - text: Weather.temp - color: Colours.palette.m3primary - horizontalAlignment: Text.AlignRight - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - elide: Text.ElideLeft - } - - StyledText { - Layout.fillWidth: true - - animate: true - text: qsTr("Feels like: %1").arg(Weather.feelsLike) - color: Colours.palette.m3outline - horizontalAlignment: Text.AlignRight - font.pointSize: Tokens.font.size.smaller - elide: Text.ElideLeft - } - } - } - } - - Loader { - id: forecastLoader - - asynchronous: true - Layout.topMargin: Tokens.spacing.smaller - Layout.bottomMargin: Tokens.padding.large * 2 - Layout.fillWidth: true - - active: root.rootHeight > 820 - visible: active - - sourceComponent: RowLayout { - spacing: Tokens.spacing.large - - Repeater { - model: { - const forecast = Weather.hourlyForecast; - const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5; - if (!forecast) - return Array.from({ - length: count - }, () => null); - - return forecast.slice(0, count); - } - - ColumnLayout { - id: forecastHour - - required property var modelData - - Layout.fillWidth: true - spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true - text: { - const hour = forecastHour.modelData?.hour ?? 0; - return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`; - } - color: Colours.palette.m3outline - horizontalAlignment: Text.AlignHCenter - font.pointSize: Tokens.font.size.larger - } - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: forecastHour.modelData?.icon ?? "cloud_alert" - font.pointSize: Tokens.font.size.extraLarge * 1.5 - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: GlobalConfig.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` - color: Colours.palette.m3secondary - font.pointSize: Tokens.font.size.larger - } - } - } - } + implicitHeight: { + const base = brief.implicitHeight + brief.anchors.topMargin; + if (showForecast) + return base + Tokens.spacing.largeIncreased + forecast.implicitHeight + forecast.anchors.margins; + return base + brief.anchors.topMargin; } + radius: Tokens.rounding.extraExtraLarge + color: Colours.tPalette.m3surfaceContainer Timer { running: true @@ -175,4 +26,28 @@ ColumnLayout { interval: 900000 // 15 minutes onTriggered: Weather.reload() } + + BriefInfo { + id: brief + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Tokens.padding.extraLarge + + rootHeight: root.rootHeight + } + + Loader { + id: forecast + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Tokens.padding.large + + active: root.showForecast + asynchronous: true + + sourceComponent: Forecast {} + } } diff --git a/modules/lock/center/Clock.qml b/modules/lock/center/Clock.qml new file mode 100644 index 00000000..d927d9af --- /dev/null +++ b/modules/lock/center/Clock.qml @@ -0,0 +1,93 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia.Config +import qs.components +import qs.services + +Item { + id: root + + required property real centerScale + + function calcTopOff(metrics: TextMetrics): real { + return metrics.tightBoundingRect.y - metrics.boundingRect.y; + } + + implicitWidth: hours.implicitWidth + minutes.implicitWidth + Tokens.spacing.small + implicitHeight: hourMetrics.tightBoundingRect.height + + StyledText { + id: hours + + y: -root.calcTopOff(hourMetrics) + text: Time.hourStr + color: Colours.palette.m3primary + font: Tokens.font.headline.builders.large.scale(7 * root.centerScale).width(30).build() + + TextMetrics { + id: hourMetrics + + text: hours.text + font: hours.font + } + } + + StyledText { + id: minutes + + anchors.right: parent.right + y: -root.calcTopOff(minuteMetrics) + + text: Time.minuteStr + color: Colours.palette.m3secondary + font: Tokens.font.headline.builders.large.scale((GlobalConfig.services.useTwelveHourClock ? 3.8 : 7) * root.centerScale).width(30).build() + + TextMetrics { + id: minuteMetrics + + text: minutes.text + font: minutes.font + } + } + + Loader { + anchors.left: minutes.left + anchors.leftMargin: minuteMetrics.tightBoundingRect.x + y: hourMetrics.tightBoundingRect.height - implicitHeight + + active: GlobalConfig.services.useTwelveHourClock + asynchronous: true + + sourceComponent: StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh + radius: Tokens.rounding.large + + implicitWidth: minuteMetrics.tightBoundingRect.width + implicitHeight: amPmMetrics.tightBoundingRect.height + Tokens.padding.large * 2 + + StyledText { + id: amPm + + anchors.centerIn: parent + width: amPmMetrics.tightBoundingRect.width + height: amPmMetrics.tightBoundingRect.height + transform: Translate { + x: -amPmMetrics.tightBoundingRect.x + y: -root.calcTopOff(amPmMetrics) + } + + text: Time.amPmStr + color: Colours.palette.m3onSurface + font: Tokens.font.headline.builders.small.scale(2 * root.centerScale).width(30).build() + + TextMetrics { + id: amPmMetrics + + text: amPm.text + font: amPm.font + } + } + } + } +} diff --git a/modules/lock/InputField.qml b/modules/lock/center/InputField.qml similarity index 88% rename from modules/lock/InputField.qml rename to modules/lock/center/InputField.qml index 1b6c34a7..78ba7bb6 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/center/InputField.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Layouts import Quickshell import Caelestia.Config import qs.components import qs.services +import qs.modules.lock Item { id: root @@ -14,9 +14,6 @@ Item { readonly property alias placeholder: placeholder property string buffer - Layout.fillWidth: true - Layout.fillHeight: true - clip: true Connections { @@ -49,13 +46,14 @@ Item { animate: true color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium opacity: root.buffer ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -74,10 +72,10 @@ Item { anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 implicitWidth: fullWidth - implicitHeight: Tokens.font.size.normal + implicitHeight: Tokens.font.body.medium.pointSize orientation: Qt.Horizontal - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall interactive: false model: ScriptModel { @@ -91,7 +89,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Tokens.rounding.small / 2 + radius: Tokens.rounding.medium / 2 opacity: 0 scale: 0 @@ -111,6 +109,7 @@ Item { } ParallelAnimation { Anim { + type: Anim.DefaultEffects target: ch property: "opacity" to: 0 @@ -129,7 +128,9 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { diff --git a/modules/lock/center/PasswordInput.qml b/modules/lock/center/PasswordInput.qml new file mode 100644 index 00000000..fb01d53c --- /dev/null +++ b/modules/lock/center/PasswordInput.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + required property int centerWidth + required property var lock + + implicitWidth: centerWidth * 0.8 + implicitHeight: input.implicitHeight + Tokens.padding.small + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.full + + focus: true + onActiveFocusChanged: { + if (!activeFocus) + forceActiveFocus(); + } + + Keys.onPressed: event => { + if (root.lock.unlocking) + return; + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) + inputField.placeholder.animate = false; + + root.lock.pam.handleKey(event); + } + + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + onClicked: parent.forceActiveFocus() + } + + RowLayout { + id: input + + anchors.fill: parent + anchors.margins: Tokens.padding.extraSmall + spacing: Tokens.spacing.medium + + Item { + Layout.fillHeight: true + implicitWidth: height + + MaterialIcon { + id: fprintIcon + + anchors.centerIn: parent + animate: true + text: { + if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) + return "fingerprint_off"; + if (root.lock.pam.fprint.active) + return "fingerprint"; + return "lock"; + } + color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface + opacity: root.lock.pam.passwd.active ? 0 : 1 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Loader { + anchors.fill: parent + anchors.margins: Tokens.padding.small + + active: opacity > 0 + opacity: root.lock.pam.passwd.active ? 1 : 0 + + sourceComponent: LoadingIndicator {} + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + } + + InputField { + id: inputField + + Layout.fillWidth: true + Layout.fillHeight: true + + pam: root.lock.pam + } + + StyledRect { + implicitWidth: implicitHeight + implicitHeight: enterIcon.implicitHeight + Tokens.padding.small + + color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + radius: Tokens.rounding.full + + StateLayer { + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + onClicked: root.lock.pam.passwd.start() + } + + MaterialIcon { + id: enterIcon + + anchors.centerIn: parent + text: "arrow_forward" + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + fontStyle: Tokens.font.icon.size(Tokens.font.icon.large.pointSize).weight(Font.Medium).build() + } + } + } +} diff --git a/modules/lock/center/ProfilePic.qml b/modules/lock/center/ProfilePic.qml new file mode 100644 index 00000000..dafd7ba8 --- /dev/null +++ b/modules/lock/center/ProfilePic.qml @@ -0,0 +1,56 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import M3Shapes +import Caelestia.Config +import qs.components +import qs.components.effects +import qs.components.images +import qs.services +import qs.utils + +Item { + id: root + + required property int centerWidth + readonly property color bgColour: Colours.tPalette.m3surfaceContainerHighest + + implicitWidth: Math.round(centerWidth * 0.7) + implicitHeight: { + shape.height; // Force update when shape height changes + return shape.pathBounds().height; + } + + MaterialShape { + id: shape + + anchors.centerIn: parent + implicitSize: root.implicitWidth + + shape: MaterialShape.ClamShell + color: Qt.alpha(root.bgColour, 1) + opacity: root.bgColour.a + layer.enabled: true + } + + MaterialIcon { + anchors.centerIn: parent + + text: "person" + color: Colours.palette.m3onSurfaceVariant + fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build() + visible: pfp.status !== Image.Ready + } + + CachingImage { + id: pfp + + anchors.fill: shape + path: `${Paths.home}/.face` + + layer.enabled: true + layer.effect: Mask { + maskSource: shape + } + } +} diff --git a/modules/lock/center/StateMessage.qml b/modules/lock/center/StateMessage.qml new file mode 100644 index 00000000..40b09643 --- /dev/null +++ b/modules/lock/center/StateMessage.qml @@ -0,0 +1,211 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.lock + +Item { + id: root + + required property Pam pam + + readonly property string msg: { + if (pam.fprintState === "error") + return qsTr("FP ERROR: %1").arg(pam.fprint.message); + if (pam.state === "error") + return qsTr("PW ERROR: %1").arg(pam.passwd.message); + + if (pam.lockMessage) + return pam.lockMessage; + + if (pam.state === "max" && pam.fprintState === "max") + return qsTr("Maximum password and fingerprint attempts reached."); + if (pam.state === "max") { + if (pam.fprint.available) + return qsTr("Maximum password attempts reached. Please use fingerprint."); + return qsTr("Maximum password attempts reached."); + } + if (pam.fprintState === "max") + return qsTr("Maximum fingerprint attempts reached. Please use password."); + + if (pam.state === "fail") { + if (pam.fprint.available) + return qsTr("Incorrect password. Please try again or use fingerprint."); + return qsTr("Incorrect password. Please try again."); + } + if (pam.fprintState === "fail") + return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); + + return ""; + } + + readonly property string stateMsg: { + if (Hypr.kbLayout !== Hypr.defaultKbLayout) { + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.capsLock) + return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.numLock) + return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); + } + + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON."); + if (Hypr.capsLock) + return qsTr("Caps lock is ON."); + if (Hypr.numLock) + return qsTr("Num lock is ON."); + + return ""; + } + + property bool stateMsgShouldBeVisible + + onMsgChanged: { + if (msg) { + if (message.opacity > 0) { + message.animate = true; + message.text = msg; + message.animate = false; + + exitAnim.stop(); + if (message.scale < 1) + appearAnim.restart(); + else + flashAnim.restart(); + } else { + message.text = msg; + exitAnim.stop(); + appearAnim.restart(); + } + } else { + appearAnim.stop(); + flashAnim.stop(); + exitAnim.start(); + } + } + + onStateMsgChanged: { + if (stateMsg) { + if (stateMessage.opacity > 0) { + stateMessage.animate = true; + stateMessage.text = stateMsg; + stateMessage.animate = false; + } else { + stateMessage.text = stateMsg; + } + stateMsgShouldBeVisible = true; + } else { + stateMsgShouldBeVisible = false; + } + } + + implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) + + Behavior on implicitHeight { + Anim {} + } + + StyledText { + id: stateMessage + + anchors.left: parent.left + anchors.right: parent.right + + scale: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0.7 + opacity: root.stateMsgShouldBeVisible && !root.msg ? 1 : 0 + color: Colours.palette.m3onSurfaceVariant + + font: Tokens.font.mono.small + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + lineHeight: 1.2 + + Behavior on scale { + Anim {} + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + StyledText { + id: message + + anchors.left: parent.left + anchors.right: parent.right + + scale: 0.7 + opacity: 0 + color: Colours.palette.m3error + + font: Tokens.font.mono.small + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + Connections { + function onFlashMsg(): void { + exitAnim.stop(); + if (message.scale < 1) + appearAnim.restart(); + else + flashAnim.restart(); + } + + target: root.pam + } + + Anim { + id: appearAnim + + type: Anim.DefaultEffects + target: message + properties: "scale,opacity" + to: 1 + onFinished: flashAnim.restart() + } + + SequentialAnimation { + id: flashAnim + + loops: 2 + + FlashAnim { + to: 0.3 + } + FlashAnim { + to: 1 + } + } + + ParallelAnimation { + id: exitAnim + + Anim { + target: message + property: "scale" + to: 0.7 + type: Anim.StandardLarge + } + Anim { + target: message + property: "opacity" + to: 0 + type: Anim.StandardLarge + } + } + } + + component FlashAnim: NumberAnimation { + target: message + property: "opacity" + duration: Tokens.anim.durations.small + easing.type: Easing.Linear + } +} diff --git a/modules/lock/weather/BriefInfo.qml b/modules/lock/weather/BriefInfo.qml new file mode 100644 index 00000000..7d87dfc1 --- /dev/null +++ b/modules/lock/weather/BriefInfo.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services + +ColumnLayout { + id: root + + required property int rootHeight + + spacing: Tokens.spacing.extraSmall + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: Weather.description + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.medium + + StyledText { + id: temp + + animate: true + text: Weather.temp + color: Colours.palette.m3primary + font: Tokens.font.headline.builders.large.scale(1.5).weight(Font.DemiBold).width(80).build() + } + + MaterialIcon { + animate: true + text: Weather.icon + color: Colours.palette.m3secondary + fontStyle: Tokens.font.headline.builders.large.scale(1.5).build() + } + } + + StyledText { + visible: root.rootHeight > Tokens.sizes.lock.showWeatherDetailsHeight + Layout.alignment: Qt.AlignHCenter + animate: true + text: qsTr("Feels like %1").arg(Weather.temp) + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.large + } + + StyledText { + visible: root.rootHeight > Tokens.sizes.lock.showWeatherDetailsHeight + Layout.alignment: Qt.AlignHCenter + animate: true + text: { + const today = Weather.forecast[0]; + return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC)); + } + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + } +} diff --git a/modules/lock/weather/Forecast.qml b/modules/lock/weather/Forecast.qml new file mode 100644 index 00000000..d23d357e --- /dev/null +++ b/modules/lock/weather/Forecast.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Layouts +import M3Shapes +import Caelestia +import Caelestia.Config +import qs.components +import qs.services + +StyledRect { + id: root + + color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + radius: Tokens.rounding.extraLargeIncreased + implicitHeight: header.anchors.margins + header.implicitHeight + Tokens.spacing.medium + layout.implicitHeight + layout.anchors.bottomMargin + + RowLayout { + id: header + + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Tokens.padding.largeIncreased + + spacing: Tokens.spacing.small + + MaterialIcon { + Layout.topMargin: Math.round(fontInfo.pointSize * 0.12) + text: "schedule" + fontStyle: Tokens.font.icon.builders.medium.weight(title.font.weight).build() + } + + StyledText { + id: title + + text: qsTr("Hourly forecast") + font: Tokens.font.title.medium + } + } + + RowLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: Tokens.padding.largeIncreased + anchors.margins: Tokens.padding.large + + spacing: Tokens.spacing.small + + Repeater { + model: CUtils.clamp(Math.floor((layout.width + layout.spacing) / (Tokens.sizes.lock.forecastItemWidth + layout.spacing)), 0, Weather.hourlyForecast.length) + + ColumnLayout { + id: hour + + required property int index + readonly property var cond: Weather.hourlyForecast[index] + + Layout.fillWidth: true + spacing: Tokens.spacing.extraSmall + + MaterialShape { + Layout.alignment: Qt.AlignHCenter + implicitSize: temp.implicitHeight + Tokens.padding.medium * 2 + shape: MaterialShape.Cookie4Sided + color: Qt.alpha(Colours.palette.m3primary, hour.index === 0 ? 1 : 0) + + Behavior on color { + CAnim {} + } + + StyledText { + id: temp + + anchors.centerIn: parent + text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F + color: hour.index === 0 ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + font: Tokens.font.title.medium + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: hour.cond.icon + color: Colours.palette.m3secondary + fontStyle: Tokens.font.icon.large + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: hour.cond.precipChance + "%" + color: Colours.palette.m3primary + } + + StyledText { + Layout.topMargin: Tokens.spacing.extraSmall + Layout.alignment: Qt.AlignHCenter + text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), GlobalConfig.services.useTwelveHourClock ? "ha" : "hh00") + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + } + } + } + } +} diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index aeaa7dea..df8418c4 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,11 +1,15 @@ +pragma ComponentBehavior: Bound + import QtQuick import Quickshell import Quickshell.Widgets +import Caelestia import Caelestia.Config import qs.components import qs.components.containers import qs.components.widgets import qs.services +import qs.modules.utilities as Utilities Item { id: root @@ -13,43 +17,53 @@ Item { required property DrawerVisibilities visibilities required property Item osdPanel required property Item sessionPanel + required property Item utilitiesPanel readonly property int padding: Tokens.padding.large + readonly property int clampedPadding: CUtils.clamp(padding - Config.border.thickness, 0, padding) anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: Tokens.sizes.notifs.width + padding * 2 + implicitWidth: Tokens.sizes.notifs.width implicitHeight: { const count = list.count; if (count === 0) return 0; - let height = (count - 1) * Tokens.spacing.smaller; + let height = (count - 1) * Tokens.spacing.medium; for (let i = 0; i < count; i++) height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; if (visibilities.osd) { - const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; + const h = osdPanel.y - clampedPadding; if (height > h) height = h; } if (visibilities.session) { - const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; + const h = sessionPanel.y - clampedPadding; if (height > h) height = h; } - return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + if (visibilities.utilities) { + const h = ((QsWindow.window as QsWindow)?.screen.height ?? 0) - (utilitiesPanel as Utilities.Wrapper).nonAnimHeight - Config.border.thickness * 2 - padding * 2 - Tokens.spacing.extraLarge; + if (height > h) + height = h; + } + + return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) + padding - clampedPadding * 2 - Config.border.thickness, height + padding + clampedPadding); } ClippingWrapperRectangle { anchors.fill: parent anchors.margins: root.padding + anchors.topMargin: root.clampedPadding + anchors.rightMargin: root.clampedPadding color: "transparent" - radius: Tokens.rounding.normal + radius: Tokens.rounding.large StyledListView { id: list @@ -89,9 +103,9 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.medium; - if (height - Tokens.spacing.smaller >= scrollY) + if (height - Tokens.spacing.medium >= scrollY) return i; } @@ -110,9 +124,9 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.medium; - if (height - Tokens.spacing.smaller >= scrollY) + if (height - Tokens.spacing.medium >= scrollY) return count - i - 1; } @@ -140,7 +154,7 @@ Item { } implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.smaller) + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.medium) ListView.onRemove: removeAnim.start() @@ -170,7 +184,7 @@ Item { Anim { target: notif property: "x" - to: (notif.x >= 0 ? wrapper.Tokens.sizes.notifs.width : -wrapper.Tokens.sizes.notifs.width) * 2 + to: (notif.x >= 0 ? root.implicitWidth : -root.implicitWidth) * 2 duration: Tokens.anim.durations.normal easing: Tokens.anim.emphasized } @@ -183,7 +197,7 @@ Item { ClippingRectangle { anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.smaller + anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.medium color: "transparent" radius: notif.radius @@ -194,6 +208,7 @@ Item { id: notif modelData: wrapper.modelData + implicitWidth: root.implicitWidth - root.padding - root.clampedPadding } } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index fe6fd056..5a10b8e7 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -1,12 +1,13 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Layouts import QtQuick.Shapes import Quickshell import Quickshell.Services.Notifications +import Caelestia.Components import Caelestia.Config import qs.components +import qs.components.controls import qs.components.effects import qs.services import qs.utils @@ -18,15 +19,15 @@ StyledRect { readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded: Config.notifs.openExpanded color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal - implicitWidth: Tokens.sizes.notifs.width + radius: Tokens.rounding.large + implicitHeight: inner.implicitHeight - x: Tokens.sizes.notifs.width + x: implicitWidth Component.onCompleted: { x = 0; modelData.lock(this); @@ -67,7 +68,7 @@ StyledRect { if (!containsMouse) root.modelData.timer.start(); - if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold) + if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; @@ -94,14 +95,12 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium implicitHeight: root.nonAnimHeight Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { @@ -176,14 +175,12 @@ StyledRect { asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent - anchors.horizontalCenterOffset: -Tokens.font.size.large * 0.02 - anchors.verticalCenterOffset: Tokens.font.size.large * 0.02 + anchors.verticalCenterOffset: 1 sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium } } } @@ -208,9 +205,9 @@ StyledRect { PathAngleArc { id: progressArc - radiusX: progressIndicator.width / 2 - root.Tokens.padding.small / 2 + radiusX: progressIndicator.width / 2 - root.Tokens.padding.extraSmall / 2 centerX: progressIndicator.width / 2 - radiusY: progressIndicator.height / 2 - root.Tokens.padding.small / 2 + radiusY: progressIndicator.height / 2 - root.Tokens.padding.extraSmall / 2 centerY: progressIndicator.height / 2 startAngle: -90 @@ -230,18 +227,20 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Tokens.spacing.smaller + anchors.leftMargin: Tokens.spacing.medium animate: true text: appNameMetrics.elidedText maximumLineCount: 1 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.label.medium opacity: root.expanded ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -249,8 +248,7 @@ StyledRect { id: appNameMetrics text: root.modelData.appName - font.family: appName.font.family - font.pointSize: appName.font.pointSize + font: appName.font elide: Text.ElideRight elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } @@ -260,7 +258,7 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Tokens.spacing.smaller + anchors.leftMargin: Tokens.spacing.medium animate: true text: summaryMetrics.elidedText @@ -273,6 +271,9 @@ StyledRect { PropertyChanges { summary.maximumLineCount: undefined + summary.anchors.topMargin: root.Tokens.spacing.extraSmall + bodyPreview.anchors.topMargin: root.Tokens.spacing.extraSmall + body.anchors.topMargin: root.Tokens.spacing.extraSmall } AnchorChanges { @@ -286,9 +287,10 @@ StyledRect { target: summary property: "maximumLineCount" } - AnchorAnim { - type: AnchorAnim.Standard + Anim { + property: "topMargin" } + AnchorAnim {} } Behavior on height { @@ -300,8 +302,7 @@ StyledRect { id: summaryMetrics text: root.modelData.summary - font.family: summary.font.family - font.pointSize: summary.font.pointSize + font: summary.font elide: Text.ElideRight elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } @@ -315,7 +316,7 @@ StyledRect { text: "•" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small states: State { name: "expanded" @@ -328,9 +329,7 @@ StyledRect { } transitions: Transition { - AnchorAnim { - type: AnchorAnim.Standard - } + AnchorAnim {} } } @@ -345,7 +344,7 @@ StyledRect { horizontalAlignment: Text.AlignLeft text: root.modelData.timeStr color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } Item { @@ -353,9 +352,10 @@ StyledRect { anchors.right: parent.right anchors.top: parent.top + anchors.topMargin: -Tokens.padding.extraSmall - implicitWidth: expandIcon.height - implicitHeight: expandIcon.height + implicitWidth: expandIcon.implicitHeight + implicitHeight: expandIcon.implicitHeight StateLayer { radius: Tokens.rounding.full @@ -367,10 +367,18 @@ StyledRect { id: expandIcon anchors.centerIn: parent + anchors.verticalCenterOffset: root.expanded ? -1 : 1 + text: "expand_more" + fontStyle: Tokens.font.icon.medium + rotation: root.expanded ? 180 : 0 - animate: true - text: root.expanded ? "expand_less" : "expand_more" - font.pointSize: Tokens.font.size.normal + Behavior on anchors.verticalCenterOffset { + Anim {} + } + + Behavior on rotation { + Anim {} + } } } @@ -386,12 +394,14 @@ StyledRect { textFormat: root.bodyTextFormat text: bodyPreviewMetrics.elidedText color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small opacity: root.expanded ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -399,8 +409,7 @@ StyledRect { id: bodyPreviewMetrics text: root.modelData.body - font.family: bodyPreview.font.family - font.pointSize: bodyPreview.font.pointSize + font: bodyPreview.font elide: Text.ElideRight elideWidth: bodyPreview.width } @@ -417,7 +426,7 @@ StyledRect { textFormat: root.bodyTextFormat text: root.modelData.body color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere height: text ? implicitHeight : 0 @@ -432,85 +441,85 @@ StyledRect { opacity: root.expanded ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } - RowLayout { + ButtonRow { id: actions - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: body.left + anchors.right: body.right anchors.top: body.bottom anchors.topMargin: Tokens.spacing.small - spacing: Tokens.spacing.smaller - + spacing: Tokens.spacing.extraSmall opacity: root.expanded ? 1 : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } - Action { - modelData: QtObject { - readonly property string text: qsTr("Close") - - function invoke(): void { - root.modelData.close(); - } - } + IconButton { + isRound: true + shapeMorph: true + fillWidth: root.modelData.actions.length === 0 + inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + icon: "close" + padding: Tokens.padding.extraSmall + onClicked: root.modelData.close() } Repeater { model: root.modelData.actions - delegate: Component { - Action {} + TextButton { + required property var modelData + + isRound: true + shapeMorph: true + fillWidth: true + inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + text: modelData.text + onClicked: modelData.invoke() + + label.horizontalAlignment: Text.AlignHCenter + label.anchors.left: left + label.anchors.right: right + label.anchors.verticalCenter: verticalCenter + label.anchors.centerIn: undefined + label.anchors.margins: Tokens.padding.medium + label.elide: Text.ElideRight + } + } + + IconButton { + isRound: true + shapeMorph: true + fillWidth: root.modelData.actions.length === 0 + inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant + icon: copyTimer.running ? "inventory" : "content_copy" + padding: Tokens.padding.extraSmall + onClicked: { + Quickshell.clipboardText = root.modelData.body; + copyTimer.restart(); + } + label.animate: true + + Timer { + id: copyTimer + + interval: 3000 } } } } } - - component Action: StyledRect { - id: action - - required property var modelData - - radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - - Layout.preferredWidth: actionText.width + Tokens.padding.normal * 2 - Layout.preferredHeight: actionText.height + Tokens.padding.small * 2 - implicitWidth: actionText.width + Tokens.padding.normal * 2 - implicitHeight: actionText.height + Tokens.padding.small * 2 - - StateLayer { - radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface - onClicked: action.modelData.invoke() - } - - StyledText { - id: actionText - - anchors.centerIn: parent - text: actionTextMetrics.elidedText - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small - } - - TextMetrics { - id: actionTextMetrics - - text: action.modelData.text - font.family: actionText.font.family - font.pointSize: actionText.font.pointSize - elide: Text.ElideRight - elideWidth: { - const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - root.Tokens.padding.normal * 2; - } - } - } } diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 97417e9c..2c6ebea6 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -8,6 +8,7 @@ Item { required property Item sidebarPanel property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel + property alias utilitiesPanel: content.utilitiesPanel visible: height > 0 anchors.topMargin: -5 @@ -17,6 +18,7 @@ Item { Content { id: content + anchors.topMargin: -root.anchors.topMargin visibilities: root.visibilities } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 3ad3ef88..1aac8558 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia import Caelestia.Config import qs.components import qs.components.controls @@ -20,14 +21,15 @@ Item { required property bool sourceMuted required property real brightness - implicitWidth: layout.implicitWidth + Tokens.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.large + layout.anchors.horizontalCenterOffset * 2 implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ColumnLayout { id: layout anchors.centerIn: parent - spacing: Tokens.spacing.normal + anchors.horizontalCenterOffset: CUtils.clamp(Tokens.padding.large - Config.border.thickness, 0, Tokens.padding.large) / 2 + spacing: Tokens.spacing.medium // Speaker volume CustomMouseArea { @@ -122,7 +124,9 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 3ea0e1a3..b6f058b8 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -45,9 +45,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Connections { diff --git a/modules/session/Content.qml b/modules/session/Content.qml index e1ba2809..f70e6493 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -2,8 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia import Caelestia.Config import qs.components +import qs.components.controls import qs.services import qs.utils @@ -13,6 +15,7 @@ Column { required property DrawerVisibilities visibilities padding: Tokens.padding.large + rightPadding: CUtils.clamp(padding - Config.border.thickness, 0, padding) spacing: Tokens.spacing.large SessionButton { @@ -76,17 +79,19 @@ Column { KeyNavigation.up: hibernate } - component SessionButton: StyledRect { + component SessionButton: IconButton { id: button - required property string icon required property list command implicitWidth: Tokens.sizes.session.button implicitHeight: Tokens.sizes.session.button - radius: Tokens.rounding.large - color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + inactiveColour: activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + inactiveOnColour: activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + radius: pressed ? Tokens.rounding.medium : activeFocus ? Tokens.rounding.extraLarge : Tokens.rounding.largeIncreased + font: Tokens.font.icon.builders.large.scale(1.3).build() + onClicked: Quickshell.execDetached(button.command) Keys.onEnterPressed: Quickshell.execDetached(button.command) Keys.onReturnPressed: Quickshell.execDetached(button.command) @@ -113,20 +118,5 @@ Column { } } } - - StateLayer { - radius: parent.radius - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - onClicked: Quickshell.execDetached(button.command) - } - - MaterialIcon { - anchors.centerIn: parent - - text: button.icon - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 41d9ba1a..aab2118c 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -22,9 +22,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index f6b8eb05..f5cde346 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -14,13 +14,13 @@ Item { id: layout anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { Layout.fillWidth: true Layout.fillHeight: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainerLow NotifDock { diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 2a390926..a14f5aa1 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -16,11 +16,11 @@ StyledRect { required property DrawerVisibilities visibilities readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null - readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.normal * 2 : summaryHeightMetrics.height + readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: { const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); return expanded ? c : Qt.alpha(c, 0); @@ -32,12 +32,12 @@ StyledRect { name: "expanded" PropertyChanges { - summary.anchors.margins: root.Tokens.padding.normal - dummySummary.anchors.margins: root.Tokens.padding.normal - compactBody.anchors.margins: root.Tokens.padding.normal - timeStr.anchors.margins: root.Tokens.padding.normal - expandedContent.anchors.margins: root.Tokens.padding.normal - summary.width: root.width - root.Tokens.padding.normal * 2 - timeStr.implicitWidth - root.Tokens.spacing.small + summary.anchors.margins: root.Tokens.padding.medium + dummySummary.anchors.margins: root.Tokens.padding.medium + compactBody.anchors.margins: root.Tokens.padding.medium + timeStr.anchors.margins: root.Tokens.padding.medium + expandedContent.anchors.margins: root.Tokens.padding.medium + summary.width: root.width - root.Tokens.padding.medium * 2 - timeStr.implicitWidth - root.Tokens.spacing.small summary.maximumLineCount: Number.MAX_SAFE_INTEGER } } @@ -106,7 +106,7 @@ StyledRect { animate: true text: root.modelData?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } } @@ -117,21 +117,19 @@ StyledRect { anchors.top: summary.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Tokens.spacing.small / 2 + anchors.topMargin: Tokens.spacing.extraSmall sourceComponent: ExpandedBody {} } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } component ExpandedBody: ColumnLayout { readonly property alias body: bodyText - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium StyledText { id: bodyText @@ -182,6 +180,7 @@ StyledRect { property: "active" } Anim { + type: Anim.DefaultEffects property: "opacity" } } @@ -192,6 +191,7 @@ StyledRect { SequentialAnimation { Anim { + type: Anim.DefaultEffects property: "opacity" } PropertyAction { diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 084fe678..ed787b66 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -20,7 +20,7 @@ Item { layer.enabled: true layer.smooth: true - layer.effect: OpacityMask { + layer.effect: Mask { maskSource: gradientMask } @@ -65,7 +65,9 @@ Item { opacity: flickable.contentX > 0 ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -78,7 +80,9 @@ Item { opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } } @@ -114,11 +118,11 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: actionInner.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: actionInner.implicitHeight + Tokens.padding.small * 2 + implicitWidth: actionInner.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: actionInner.implicitHeight + Tokens.padding.small Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0) - radius: actionStateLayer.pressed ? Tokens.rounding.small / 2 : Tokens.rounding.small + radius: actionStateLayer.pressed ? Tokens.rounding.medium / 2 : Tokens.rounding.medium color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) Timer { diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 4509ce26..10014a43 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -20,7 +20,7 @@ Item { readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium Component.onCompleted: Notifs.list.forEach(n => n.popup = false) @@ -30,7 +30,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.small + anchors.margins: Tokens.padding.extraSmall implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) @@ -44,16 +44,16 @@ Item { text: root.notifCount color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.label.large Behavior on anchors.leftMargin { Anim {} } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } @@ -63,13 +63,11 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: count.right anchors.right: parent.right - anchors.leftMargin: Tokens.spacing.small + anchors.leftMargin: Tokens.spacing.extraSmall text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.normal - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.label.large elide: Text.ElideRight } } @@ -81,9 +79,9 @@ Item { anchors.right: parent.right anchors.top: title.bottom anchors.bottom: parent.bottom - anchors.topMargin: Tokens.spacing.smaller + anchors.topMargin: Tokens.spacing.medium - radius: Tokens.rounding.small + radius: Tokens.rounding.medium color: "transparent" Loader { @@ -93,7 +91,7 @@ Item { opacity: root.notifCount > 0 ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Tokens.spacing.large + spacing: Tokens.spacing.extraLarge Image { asynchronous: true @@ -110,11 +108,9 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") + text: qsTr("All up to date!") color: Colours.palette.m3outlineVariant - font.pointSize: Tokens.font.size.large - font.family: Tokens.font.family.mono - font.weight: 500 + font: Tokens.font.headline.builders.small.width(90).build() } } @@ -178,7 +174,7 @@ Item { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium scale: root.notifCount > 0 ? 1 : 0.5 opacity: root.notifCount > 0 ? 1 : 0 @@ -188,9 +184,7 @@ Item { id: clearBtn icon: "clear_all" - radius: Tokens.rounding.normal - padding: Tokens.padding.normal - font.pointSize: Math.round(Tokens.font.size.large * 1.2) + font: Tokens.font.icon.large onClicked: clearTimer.start() Elevation { @@ -209,7 +203,7 @@ Item { Behavior on opacity { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial + type: Anim.DefaultEffects } } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 2677698b..aa9a962a 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -128,25 +128,21 @@ LazyListView { Behavior on y { enabled: notif.LazyListView.ready - Anim { - type: Anim.DefaultSpatial - } - } - - Behavior on opacity { Anim {} } - Behavior on scale { + Behavior on opacity { Anim { - type: Anim.DefaultSpatial + type: Anim.DefaultEffects } } + Behavior on scale { + Anim {} + } + Behavior on x { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } @@ -156,6 +152,5 @@ LazyListView { target: root.container property: "contentY" - type: Anim.DefaultSpatial } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 2920af74..f75b1d70 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -32,9 +32,9 @@ StyledRect { } readonly property int nonAnimHeight: { - const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); + const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0); const columnHeight = headerHeight + notifList.layoutHeight; - return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); + return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.medium * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -57,13 +57,11 @@ StyledRect { implicitHeight: nonAnimHeight clip: true - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } RowLayout { @@ -72,9 +70,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Tokens.padding.normal + anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -115,7 +113,7 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.medium } } @@ -127,6 +125,7 @@ StyledRect { Loader { asynchronous: true anchors.centerIn: parent + anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0 sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } @@ -159,7 +158,7 @@ StyledRect { id: column Layout.fillWidth: true - spacing: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 + spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0 Behavior on spacing { Anim {} @@ -170,13 +169,13 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } @@ -184,12 +183,12 @@ StyledRect { animate: true text: root.activeNotifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Tokens.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.large + implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) radius: Tokens.rounding.full @@ -203,35 +202,31 @@ StyledRect { id: expandBtn anchors.centerIn: parent - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall StyledText { id: groupCount - Layout.leftMargin: Tokens.padding.small / 2 + Layout.leftMargin: Tokens.padding.extraSmall / 2 animate: true text: root.notifCount - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Tokens.font.size.small + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small } MaterialIcon { - Layout.rightMargin: -Tokens.padding.small / 2 + Layout.rightMargin: -Tokens.padding.extraSmall / 2 text: "expand_more" - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurfaceVariant rotation: root.expanded ? 180 : 0 - Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.smaller / 2) : 0 + Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0 Behavior on rotation { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on Layout.topMargin { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 72b8b3a8..a34fe5eb 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -22,7 +22,7 @@ LazyListView { anchors.right: parent.right implicitHeight: contentHeight - spacing: Math.round(Tokens.spacing.small / 2) + spacing: Math.round(Tokens.spacing.extraSmall) asynchronous: true readyDelay: 1 @@ -107,6 +107,7 @@ LazyListView { onFinished: notif.modelData?.unlock(notif) Anim { + type: Anim.DefaultEffects target: notif property: "opacity" to: 0 @@ -131,13 +132,13 @@ LazyListView { Behavior on y { enabled: notif.LazyListView.ready - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -145,9 +146,7 @@ LazyListView { } Behavior on x { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 108c51a2..bb290fce 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia import Caelestia.Config import qs.components @@ -19,9 +20,7 @@ Item { opacity: 1 - offsetScale Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { @@ -30,13 +29,14 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left - anchors.margins: Tokens.padding.large + anchors.leftMargin: Tokens.padding.large + anchors.margins: CUtils.clamp(anchors.leftMargin - Config.border.thickness, 0, anchors.leftMargin) anchors.bottomMargin: 0 active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: Tokens.sizes.sidebar.width - Tokens.padding.large * 2 + implicitWidth: Tokens.sizes.sidebar.width - content.anchors.leftMargin - content.anchors.margins props: root.props visibilities: root.visibilities } diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index e03c546f..eadf0f8c 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -13,6 +13,8 @@ Item { required property BarPopouts.Wrapper popouts required property matrix4x4 deformMatrix + readonly property real nonAnimHeight: idleInhibit.nonAnimHeight + record.nonAnimHeight + toggles.implicitHeight + layout.spacing * 2 + implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight @@ -20,17 +22,23 @@ Item { id: layout anchors.fill: parent - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium - IdleInhibit {} + IdleInhibit { + id: idleInhibit + } Record { + id: record + props: root.props visibilities: root.visibilities z: 1 } Toggles { + id: toggles + visibilities: root.visibilities popouts: root.popouts } diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 13125f2c..2e462b46 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -43,7 +43,7 @@ Loader { anchors.fill: parent anchors.rightMargin: -parent.width * (1 - root.deformMatrix.m11) / 2 // Additional bit to account for deform anchors.bottomMargin: -parent.height * 0.1 // Additional bit to account for overshoot - topLeftRadius: Tokens.rounding.large + topLeftRadius: Tokens.rounding.extraLarge color: Colours.palette.m3scrim } @@ -138,15 +138,15 @@ Loader { StyledRect { anchors.centerIn: parent - radius: Tokens.rounding.large + radius: Tokens.rounding.extraLarge color: Colours.palette.m3surfaceContainerHigh scale: 0 Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) - width: Math.min(parent.width - Tokens.padding.large * 2, implicitWidth) - implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.large * 3 - implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.large * 3 + width: Math.min(parent.width - Tokens.padding.extraLargeIncreased, implicitWidth) + implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.extraExtraLarge + implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.extraExtraLarge MouseArea { anchors.fill: parent @@ -164,25 +164,25 @@ Loader { anchors.fill: parent anchors.margins: Tokens.padding.large * 1.5 - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Delete recording?") - font.pointSize: Tokens.font.size.large + font: Tokens.font.body.large } StyledText { Layout.fillWidth: true text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { - Layout.topMargin: Tokens.spacing.normal + Layout.topMargin: Tokens.spacing.medium Layout.alignment: Qt.AlignRight - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium TextButton { text: qsTr("Cancel") @@ -202,14 +202,14 @@ Loader { } Behavior on scale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 958d90e6..2a9af11c 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia import Caelestia.Config import qs.components import qs.modules.sidebar as Sidebar @@ -24,12 +25,14 @@ Item { reloadableId: "utilities" } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) + readonly property real totalPadding: content.anchors.margins + CUtils.clamp(content.anchors.margins - Config.border.thickness, 0, content.anchors.margins) + readonly property real nonAnimHeight: ((content.item as Content)?.nonAnimHeight ?? 0) + totalPadding property real offsetScale: shouldBeActive ? 0 : 1 property real sidebarLerp visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale - implicitHeight: content.implicitHeight + content.anchors.margins * 2 + implicitHeight: content.implicitHeight + totalPadding implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Tokens.sizes.utilities.width * (1 - sidebarLerp) opacity: 1 - offsetScale @@ -64,9 +67,7 @@ Item { ] Behavior on offsetScale { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Loader { @@ -80,7 +81,7 @@ Item { active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: root.implicitWidth - content.anchors.margins * 2 + implicitWidth: root.implicitWidth - root.totalPadding props: root.props visibilities: root.visibilities popouts: root.popouts diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index c37ef3c5..c5244c93 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -8,10 +8,12 @@ import qs.services StyledRect { id: root - Layout.fillWidth: true - implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.large * 2 + readonly property real nonAnimHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + Layout.fillWidth: true + implicitHeight: nonAnimHeight + + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer clip: true @@ -22,11 +24,11 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.large radius: Tokens.rounding.full color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer @@ -37,7 +39,7 @@ StyledRect { anchors.centerIn: parent text: "coffee" color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -48,7 +50,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Keep Awake") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } @@ -56,7 +58,7 @@ StyledRect { Layout.fillWidth: true text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } } @@ -73,7 +75,7 @@ StyledRect { asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left - anchors.topMargin: Tokens.spacing.larger + anchors.topMargin: Tokens.spacing.large anchors.bottomMargin: IdleInhibitor.enabled ? Tokens.padding.large : -implicitHeight anchors.leftMargin: Tokens.padding.large @@ -83,8 +85,8 @@ StyledRect { Component.onCompleted: active = Qt.binding(() => opacity > 0) sourceComponent: StyledRect { - implicitWidth: activeText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: activeText.implicitHeight + Tokens.padding.small * 2 + implicitWidth: activeText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: activeText.implicitHeight + Tokens.padding.small radius: Tokens.rounding.full color: Colours.palette.m3primary @@ -95,14 +97,12 @@ StyledRect { anchors.centerIn: parent text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, GlobalConfig.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) color: Colours.palette.m3onPrimary - font.pointSize: Math.round(Tokens.font.size.small * 0.9) + font: Tokens.font.body.builders.small.size(Math.round(Tokens.font.body.small.pointSize * 0.9)).build() } } Behavior on anchors.bottomMargin { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } Behavior on opacity { @@ -117,8 +117,6 @@ StyledRect { } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 43a84076..c0d7e0c6 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -12,11 +12,12 @@ StyledRect { required property var props required property DrawerVisibilities visibilities + readonly property real nonAnimHeight: btnLayout.implicitHeight + listOrControls.implicitHeight + layout.spacing + layout.anchors.margins * 2 Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -24,16 +25,17 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium RowLayout { - spacing: Tokens.spacing.normal - z: 1 + id: btnLayout + + spacing: Tokens.spacing.medium StyledRect { implicitWidth: implicitHeight implicitHeight: { - const h = icon.implicitHeight + Tokens.padding.smaller * 2; + const h = icon.implicitHeight + Tokens.padding.large; return h - (h % 2); } @@ -48,7 +50,7 @@ StyledRect { anchors.verticalCenterOffset: 1.5 text: "screen_record" color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } @@ -59,7 +61,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Screen Recorder") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium elide: Text.ElideRight } @@ -67,7 +69,7 @@ StyledRect { Layout.fillWidth: true text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Tokens.font.size.small + font: Tokens.font.body.small elide: Text.ElideRight } } @@ -187,14 +189,14 @@ StyledRect { id: recordingControls RowLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledRect { radius: Tokens.rounding.full color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error - implicitWidth: recText.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: recText.implicitHeight + Tokens.padding.smaller * 2 + implicitWidth: recText.implicitWidth + Tokens.padding.medium * 2 + implicitHeight: recText.implicitHeight + Tokens.padding.large StyledText { id: recText @@ -203,7 +205,7 @@ StyledRect { animate: true text: Recorder.paused ? "PAUSED" : "REC" color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError - font.family: Tokens.font.family.mono + font: Tokens.font.mono.medium } Behavior on implicitWidth { @@ -246,7 +248,7 @@ StyledRect { return qsTr("Recording for %1").arg(time); } - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } Item { @@ -256,10 +258,10 @@ StyledRect { IconButton { label.animate: true icon: Recorder.paused ? "play_arrow" : "pause" - toggle: true + isToggle: true checked: Recorder.paused type: IconButton.Tonal - font.pointSize: Tokens.font.size.large + font: Tokens.font.icon.large onClicked: { Recorder.togglePause(); internalChecked = Recorder.paused; @@ -270,7 +272,7 @@ StyledRect { icon: "stop" inactiveColour: Colours.palette.m3error inactiveOnColour: Colours.palette.m3onError - font.pointSize: Tokens.font.size.large + font: Tokens.font.icon.large onClicked: Recorder.stop() } } diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 951f7f09..ab727817 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -27,19 +27,19 @@ ColumnLayout { onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignVCenter text: "list" - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true text: qsTr("Recordings") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } IconButton { @@ -62,7 +62,7 @@ ColumnLayout { Layout.fillWidth: true Layout.rightMargin: -Tokens.spacing.small - implicitHeight: (Tokens.font.size.larger + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) + implicitHeight: (Tokens.font.body.large.pointSize + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -78,13 +78,13 @@ ColumnLayout { anchors.left: list.contentItem.left anchors.right: list.contentItem.right anchors.rightMargin: Tokens.spacing.small - spacing: Tokens.spacing.small / 2 + spacing: Tokens.spacing.extraSmall Component.onCompleted: baseName = modelData.baseName StyledText { Layout.fillWidth: true - Layout.rightMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.extraSmall text: { const time = recording.baseName; const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); @@ -129,31 +129,25 @@ ColumnLayout { add: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" from: 0 to: 1 } - Anim { - property: "scale" - from: 0.5 - to: 1 - } } remove: Transition { Anim { + type: Anim.DefaultEffects property: "opacity" to: 0 } - Anim { - property: "scale" - to: 0.5 - } } displaced: Transition { Anim { - properties: "opacity,scale" + type: Anim.DefaultEffects + property: "opacity" to: 1 } Anim { @@ -175,14 +169,16 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge + fontStyle: Tokens.font.icon.extraLarge opacity: root.props.recordingListExpanded ? 1 : 0 scale: root.props.recordingListExpanded ? 1 : 0 Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -195,7 +191,7 @@ ColumnLayout { } RowLayout { - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -207,7 +203,9 @@ ColumnLayout { Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0 Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -227,14 +225,14 @@ ColumnLayout { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } } Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 8c294eb3..51a59368 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth +import Caelestia.Components import Caelestia.Config import qs.components import qs.components.controls @@ -38,9 +39,9 @@ StyledRect { readonly property bool needExtraRow: quickToggles.length > 6 Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -48,31 +49,31 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { text: qsTr("Quick Toggles") - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } QuickToggleRow { - rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles + model: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles } QuickToggleRow { visible: root.needExtraRow - rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] + model: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] } } - component QuickToggleRow: RowLayout { - property var rowModel: [] + component QuickToggleRow: ButtonRow { + property alias model: repeater.model Layout.fillWidth: true spacing: Tokens.spacing.small Repeater { - model: parent.rowModel + id: repeater delegate: DelegateChooser { role: "id" @@ -114,7 +115,7 @@ StyledRect { delegate: Toggle { icon: "settings" inactiveOnColour: Colours.palette.m3onSurfaceVariant - toggle: false + isToggle: false onClicked: { root.visibilities.utilities = false; root.popouts.detach("network"); @@ -143,7 +144,7 @@ StyledRect { icon: "vpn_key" checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" enabled: !VPN.connecting - toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + isToggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" inactiveOnColour: Colours.palette.m3onSurfaceVariant onClicked: VPN.toggle() } @@ -153,18 +154,10 @@ StyledRect { } component Toggle: IconButton { - Layout.fillWidth: true - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) - radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - toggle: true - radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial - radiusAnim.easing: Tokens.anim.expressiveFastSpatial - - Behavior on Layout.preferredWidth { - Anim { - type: Anim.FastSpatial - } - } + fillWidth: true + isToggle: true + isRound: true + shapeMorph: true } } diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 5247e773..93d68937 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -13,9 +13,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3successContainer; @@ -50,13 +50,13 @@ StyledRect { id: layout anchors.fill: parent - anchors.margins: Tokens.padding.smaller - anchors.leftMargin: Tokens.padding.normal - anchors.rightMargin: Tokens.padding.normal - spacing: Tokens.spacing.normal + anchors.margins: Tokens.padding.small + anchors.leftMargin: Tokens.padding.medium + anchors.rightMargin: Tokens.padding.medium + spacing: Tokens.spacing.medium StyledRect { - radius: Tokens.rounding.normal + radius: Tokens.rounding.large color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3success; @@ -68,7 +68,7 @@ StyledRect { } implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.large MaterialIcon { id: icon @@ -84,7 +84,7 @@ StyledRect { return Colours.palette.m3onError; return Colours.palette.m3onSurfaceVariant; } - font.pointSize: Math.round(Tokens.font.size.large * 1.2) + fontStyle: Tokens.font.icon.builders.large.scale(1.2).build() } } @@ -106,7 +106,7 @@ StyledRect { return Colours.palette.m3onErrorContainer; return Colours.palette.m3onSurface; } - font.pointSize: Tokens.font.size.normal + font: Tokens.font.title.small elide: Text.ElideRight } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 21f2934e..2828d6f6 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -23,7 +23,7 @@ Item { return false; } - implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.normal * 2 + implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.medium * 2 implicitHeight: { let h = -spacing; for (let i = 0; i < repeater.count; i++) { @@ -111,7 +111,6 @@ Item { properties: "opacity,scale" from: 0 to: 1 - type: Anim.DefaultSpatial } ParallelAnimation { @@ -120,6 +119,7 @@ Item { onFinished: toast.modelData.unlock(toast) Anim { + type: Anim.DefaultEffects target: toast property: "opacity" to: 0 @@ -138,7 +138,9 @@ Item { } Behavior on opacity { - Anim {} + Anim { + type: Anim.DefaultEffects + } } Behavior on scale { @@ -146,9 +148,7 @@ Item { } Behavior on anchors.bottomMargin { - Anim { - type: Anim.DefaultSpatial - } + Anim {} } } } diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index c4fbbd53..a349cbb5 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -21,7 +21,7 @@ ColumnLayout { Layout.leftMargin: Tokens.padding.large Layout.rightMargin: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium StyledText { Layout.fillWidth: true @@ -31,10 +31,10 @@ ColumnLayout { StyledRect { color: Colours.palette.m3primary - radius: Tokens.rounding.small + radius: Tokens.rounding.medium - implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small * 2 - implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small + implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small + implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.extraSmall StateLayer { color: Colours.palette.m3onPrimary @@ -49,27 +49,27 @@ ColumnLayout { animate: true text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" color: Colours.palette.m3onPrimary - font.pointSize: Tokens.font.size.large + fontStyle: Tokens.font.icon.large } } } WrapperItem { Layout.fillWidth: true - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 clip: true - topMargin: Tokens.spacing.normal - bottomMargin: Tokens.spacing.normal + topMargin: Tokens.spacing.medium + bottomMargin: Tokens.spacing.medium GridLayout { id: wsGrid - rowSpacing: Tokens.spacing.smaller - columnSpacing: Tokens.spacing.normal + rowSpacing: Tokens.spacing.medium + columnSpacing: Tokens.spacing.medium columns: 5 Repeater { @@ -103,7 +103,7 @@ ColumnLayout { Layout.rightMargin: Tokens.padding.large Layout.bottomMargin: Tokens.padding.large - spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small + spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.medium : Tokens.spacing.small Button { color: Colours.palette.m3secondaryContainer @@ -142,10 +142,10 @@ ColumnLayout { signal clicked - radius: Tokens.rounding.small + radius: Tokens.rounding.medium Layout.fillWidth: true - implicitHeight: label.implicitHeight + Tokens.padding.small * 2 + implicitHeight: label.implicitHeight + Tokens.padding.small StateLayer { id: stateLayer @@ -161,7 +161,7 @@ ColumnLayout { animate: true color: parent.onColor - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } } diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index 1820409f..50babbda 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -14,29 +14,28 @@ ColumnLayout { spacing: Tokens.spacing.small Label { - Layout.topMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.padding.extraLargeIncreased text: root.client?.title ?? qsTr("No active client") wrapMode: Text.WrapAtWordBoundaryOrAnywhere - font.pointSize: Tokens.font.size.large - font.weight: 500 + font: Tokens.font.body.builders.large.weight(Font.Medium).build() } Label { text: root.client?.lastIpcObject.class ?? qsTr("No active client") color: Colours.palette.m3tertiary - font.pointSize: Tokens.font.size.larger + font: Tokens.font.body.large } StyledRect { Layout.fillWidth: true Layout.preferredHeight: 1 - Layout.leftMargin: Tokens.padding.large * 2 - Layout.rightMargin: Tokens.padding.large * 2 - Layout.topMargin: Tokens.spacing.normal - Layout.bottomMargin: Tokens.spacing.large + Layout.leftMargin: Tokens.padding.extraLargeIncreased + Layout.rightMargin: Tokens.padding.extraLargeIncreased + Layout.topMargin: Tokens.spacing.medium + Layout.bottomMargin: Tokens.spacing.largeIncreased color: Colours.palette.m3secondary } @@ -134,7 +133,7 @@ ColumnLayout { Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true - spacing: Tokens.spacing.smaller + spacing: Tokens.spacing.medium MaterialIcon { id: icon @@ -149,7 +148,7 @@ ColumnLayout { text: detail.text elide: Text.ElideRight - font.pointSize: Tokens.font.size.normal + font: Tokens.font.body.medium } } diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index fed85891..4ff03480 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -15,7 +15,7 @@ Item { required property ShellScreen screen required property HyprlandToplevel client - Layout.preferredWidth: preview.implicitWidth + Tokens.padding.large * 2 + Layout.preferredWidth: preview.implicitWidth + Tokens.padding.extraLargeIncreased Layout.fillHeight: true StyledClippingRect { @@ -25,12 +25,12 @@ Item { anchors.top: parent.top anchors.bottom: label.top anchors.topMargin: Tokens.padding.large - anchors.bottomMargin: Tokens.spacing.normal + anchors.bottomMargin: Tokens.spacing.medium implicitWidth: view.implicitWidth color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.small + radius: Tokens.rounding.medium Loader { asynchronous: true @@ -44,22 +44,21 @@ Item { Layout.alignment: Qt.AlignHCenter text: "web_asset_off" color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge * 3 + fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No active client") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 + font: Tokens.font.body.builders.large.size(28).weight(Font.Medium).build() } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Try switching to a window") color: Colours.palette.m3outline - font.pointSize: Tokens.font.size.large + font: Tokens.font.body.large } } } diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index 7ee35c29..212cb717 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -21,7 +21,7 @@ Item { anchors.fill: parent anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Preview { screen: root.screen @@ -29,7 +29,7 @@ Item { } ColumnLayout { - spacing: Tokens.spacing.normal + spacing: Tokens.spacing.medium Layout.preferredWidth: Tokens.sizes.winfo.detailsWidth Layout.fillHeight: true @@ -39,7 +39,7 @@ Item { Layout.fillHeight: true color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Details { client: root.client @@ -51,7 +51,7 @@ Item { Layout.preferredHeight: buttons.implicitHeight color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.normal + radius: Tokens.rounding.large Buttons { id: buttons diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 062959ce..76e07c45 100644 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -1 +1,18 @@ +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) +find_package(PkgConfig REQUIRED) +pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) +pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) +pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) +pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) +if(NOT Cava_FOUND) + pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) +endif() +include(cmake/sensorslib.cmake) + +set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") +qt_standard_project_setup(REQUIRES 6.9) + +include(cmake/pch.cmake) +include(cmake/qml-module.cmake) + add_subdirectory(src/Caelestia) diff --git a/plugin/cmake/pch.cmake b/plugin/cmake/pch.cmake new file mode 100644 index 00000000..6027de6f --- /dev/null +++ b/plugin/cmake/pch.cmake @@ -0,0 +1,14 @@ +add_library(caelestia-pch INTERFACE) +target_precompile_headers(caelestia-pch INTERFACE + + + + + + + + + + + +) diff --git a/plugin/cmake/qml-module.cmake b/plugin/cmake/qml-module.cmake new file mode 100644 index 00000000..29dc5fc2 --- /dev/null +++ b/plugin/cmake/qml-module.cmake @@ -0,0 +1,52 @@ +message(STATUS "QML install dir: ${CMAKE_INSTALL_PREFIX}/${INSTALL_QMLDIR}") + +function(qml_module arg_TARGET) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;QML_SINGLETONS;DEPENDENCIES;IMPORTS;OPTIONAL_IMPORTS;DEFAULT_IMPORTS;LIBRARIES") + + set_source_files_properties(${arg_QML_SINGLETONS} PROPERTIES QT_QML_SINGLETON_TYPE TRUE) + + qt_add_qml_module(${arg_TARGET} + URI ${arg_URI} + SOURCES ${arg_SOURCES} + QML_FILES ${arg_QML_FILES} ${arg_QML_SINGLETONS} + DEPENDENCIES ${arg_DEPENDENCIES} + IMPORTS ${arg_IMPORTS} + OPTIONAL_IMPORTS ${arg_OPTIONAL_IMPORTS} + DEFAULT_IMPORTS ${arg_DEFAULT_IMPORTS} + ) + + qt_query_qml_module(${arg_TARGET} + URI module_uri + PLUGIN_TARGET module_plugin_target + TARGET_PATH module_target_path + QMLDIR module_qmldir + TYPEINFO module_typeinfo + ) + + message(STATUS "Created QML module: ${module_uri}") + + # Modules can't link together properly if the backing target and plugin target + # are in the same dir (because diff modules can't access each other). + # So install backing targets to QMLDIR/.../lib/ instead. + string(REPLACE "/" ";" uri_parts "${module_target_path}") + list(GET uri_parts 0 top_level) + set(backing_lib_dir "${INSTALL_QMLDIR}/${top_level}/lib") + set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") + + install(TARGETS ${arg_TARGET} + LIBRARY DESTINATION "${backing_lib_dir}" + RUNTIME DESTINATION "${backing_lib_dir}" + ) + install(TARGETS "${module_plugin_target}" + LIBRARY DESTINATION "${module_dir}" + RUNTIME DESTINATION "${module_dir}" + ) + install(FILES "${module_qmldir}" DESTINATION "${module_dir}") + install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") + + target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) + + # Add backing target dir to plugin rpath so it can find its backing target + file(RELATIVE_PATH plugin_to_lib "/${module_target_path}" "/${top_level}/lib") + set_property(TARGET ${module_plugin_target} APPEND PROPERTY INSTALL_RPATH "$ORIGIN/${plugin_to_lib}") +endfunction() diff --git a/plugin/cmake/sensorslib.cmake b/plugin/cmake/sensorslib.cmake new file mode 100644 index 00000000..67d9117a --- /dev/null +++ b/plugin/cmake/sensorslib.cmake @@ -0,0 +1,9 @@ +find_library(SENSORS_LIBRARY NAMES sensors REQUIRED) +find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED) +if(NOT TARGET Sensors::Sensors) + add_library(Sensors::Sensors UNKNOWN IMPORTED) + set_target_properties(Sensors::Sensors PROPERTIES + IMPORTED_LOCATION "${SENSORS_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}" + ) +endif() diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp index a4703c86..e1ff8b23 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.cpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -28,6 +28,14 @@ void BlobGroup::setColor(const QColor& c) { markDirty(); } +void BlobGroup::setCornerFill(bool e) { + if (m_cornerFill == e) + return; + m_cornerFill = e; + emit cornerFillChanged(); + markDirty(); +} + void BlobGroup::addShape(BlobShape* shape) { if (!shape || m_shapes.contains(shape)) return; diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp index e09125a9..a73ffc99 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.hpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -13,6 +13,7 @@ class BlobGroup : public QObject { QML_ELEMENT Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + Q_PROPERTY(bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY cornerFillChanged) public: explicit BlobGroup(QObject* parent = nullptr); @@ -26,6 +27,10 @@ public: void setColor(const QColor& c); + bool cornerFill() const { return m_cornerFill; } + + void setCornerFill(bool e); + void addShape(BlobShape* shape); void removeShape(BlobShape* shape); @@ -43,10 +48,12 @@ public: signals: void smoothingChanged(); void colorChanged(); + void cornerFillChanged(); private: qreal m_smoothing = 32.0; QColor m_color{ 0x44, 0x88, 0xff }; + bool m_cornerFill = true; QList m_shapes; BlobInvertedRect* m_invertedRect = nullptr; bool m_physicsUpdated = false; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index 15486d83..f78d346d 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -173,11 +173,24 @@ bool BlobRect::isExcluded(const BlobShape* other) const { return false; } +bool BlobRect::isCornerExcluded(const BlobShape* other) const { + for (const auto& ptr : m_excludeCorners) { + if (ptr == other) + return true; + } + return false; +} + QQmlListProperty BlobRect::exclude() { return QQmlListProperty( this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); } +QQmlListProperty BlobRect::excludeCorners() { + return QQmlListProperty(this, nullptr, &excludeCornersAppend, &excludeCornersCount, &excludeCornersAt, + &excludeCornersClear, &excludeCornersReplace, &excludeCornersRemoveLast); +} + void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { auto* self = static_cast(prop->object); self->m_exclude.append(rect); @@ -224,6 +237,52 @@ void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { emit self->excludeChanged(); } +void BlobRect::excludeCornersAppend(QQmlListProperty* prop, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_excludeCorners.append(rect); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +qsizetype BlobRect::excludeCornersCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_excludeCorners.size(); +} + +BlobRect* BlobRect::excludeCornersAt(QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_excludeCorners.at(index); +} + +void BlobRect::excludeCornersClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_excludeCorners.isEmpty()) + return; + self->m_excludeCorners.clear(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect::excludeCornersReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_excludeCorners[index] = rect; + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + +void BlobRect::excludeCornersRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_excludeCorners.isEmpty()) + return; + self->m_excludeCorners.removeLast(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeCornersChanged(); +} + void BlobRect::checkAtRest(float speed) { constexpr float kEpsilon = 0.002f; const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp index d2d6ad45..354cc265 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -14,6 +14,7 @@ class BlobRect : public BlobShape { Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) + Q_PROPERTY(QQmlListProperty excludeCorners READ excludeCorners NOTIFY excludeCornersChanged) Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) @@ -52,8 +53,10 @@ public: } QQmlListProperty exclude(); + QQmlListProperty excludeCorners(); bool isExcluded(const BlobShape* other) const override; + bool isCornerExcluded(const BlobShape* other) const override; void cornerRadii(float out[4]) const override; qreal topLeftRadius() const { return m_topLeftRadius; } @@ -77,6 +80,7 @@ signals: void dampingChanged(); void deformScaleChanged(); void excludeChanged(); + void excludeCornersChanged(); void topLeftRadiusChanged(); void topRightRadiusChanged(); void bottomLeftRadiusChanged(); @@ -116,6 +120,7 @@ private: qreal m_bottomRightRadius = -1; QList> m_exclude; + QList> m_excludeCorners; static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); static qsizetype excludeCount(QQmlListProperty* prop); @@ -123,4 +128,11 @@ private: static void excludeClear(QQmlListProperty* prop); static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); static void excludeRemoveLast(QQmlListProperty* prop); + + static void excludeCornersAppend(QQmlListProperty* prop, BlobRect* rect); + static qsizetype excludeCornersCount(QQmlListProperty* prop); + static BlobRect* excludeCornersAt(QQmlListProperty* prop, qsizetype index); + static void excludeCornersClear(QQmlListProperty* prop); + static void excludeCornersReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); + static void excludeCornersRemoveLast(QQmlListProperty* prop); }; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index cfa11c44..8eed3653 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -288,10 +288,12 @@ void BlobShape::updatePolish() { // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) const float smoothFactor = pad; constexpr float minR = 2.0f; + const bool cornerFill = m_group->cornerFill(); const auto rectCount = m_cachedRects.size(); for (qsizetype i = 0; i < rectCount; ++i) { auto& ri = m_cachedRects[i]; const int riExcludeMask = ri.excludeMask; + BlobShape* const si = rectShapes[i]; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; @@ -299,19 +301,32 @@ void BlobShape::updatePolish() { const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; - for (qsizetype j = 0; j < rectCount; ++j) { + for (qsizetype j = 0; cornerFill && j < rectCount; ++j) { if (j == i) continue; if (riExcludeMask & (1 << j)) continue; + BlobShape* const sj = rectShapes[j]; + if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) + continue; const auto& rj = m_cachedRects[j]; - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + // Skip when the corner is inside rj: it's buried, not on a visible junction, + // so squaring it would only perturb interior SDF gradients. + const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh); + const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh); + const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh); + const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh); + if (sdTr >= 0.0f) + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, sdTr)); + if (sdBr >= 0.0f) + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, sdBr)); + if (sdBl >= 0.0f) + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, sdBl)); + if (sdTl >= 0.0f) + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, sdTl)); } - if (m_cachedHasInverted) { + if (cornerFill && m_cachedHasInverted) { const float icx = m_cachedInvertedInner[0]; const float icy = m_cachedInvertedInner[1]; const float ihw = m_cachedInvertedInner[2]; diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index c05a40d8..e179531a 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -49,6 +49,8 @@ protected: virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } + virtual bool isCornerExcluded(const BlobShape* /*other*/) const { return false; } + virtual void cornerRadii(float out[4]) const; virtual void updatePhysics() {} diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 6e73f4fe..0ae917bd 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,61 +1,3 @@ -find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) -find_package(PkgConfig REQUIRED) -pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) -pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) -pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) -pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) -if(NOT Cava_FOUND) - pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) -endif() - -set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") -qt_standard_project_setup(REQUIRES 6.9) - -function(qml_module arg_TARGET) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;LIBRARIES") - - qt_add_qml_module(${arg_TARGET} - URI ${arg_URI} - VERSION ${VERSION} - SOURCES ${arg_SOURCES} - QML_FILES ${arg_QML_FILES} - ) - - qt_query_qml_module(${arg_TARGET} - URI module_uri - VERSION module_version - PLUGIN_TARGET module_plugin_target - TARGET_PATH module_target_path - QMLDIR module_qmldir - TYPEINFO module_typeinfo - ) - - message(STATUS "Created QML module ${module_uri}, version ${module_version}") - - set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") - install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") - install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") - install(FILES "${module_qmldir}" DESTINATION "${module_dir}") - install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") - - target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) -endfunction() - -add_library(caelestia-pch INTERFACE) -target_precompile_headers(caelestia-pch INTERFACE - - - - - - - - - - - -) - qml_module(caelestia URI Caelestia SOURCES diff --git a/plugin/src/Caelestia/Components/CMakeLists.txt b/plugin/src/Caelestia/Components/CMakeLists.txt index f880d318..ac8ce166 100644 --- a/plugin/src/Caelestia/Components/CMakeLists.txt +++ b/plugin/src/Caelestia/Components/CMakeLists.txt @@ -1,7 +1,9 @@ qml_module(caelestia-components URI Caelestia.Components SOURCES - lazylistview.hpp lazylistview.cpp + lazylistview.cpp + wavyline.cpp + buttonrow.cpp LIBRARIES Qt::Quick ) diff --git a/plugin/src/Caelestia/Components/buttonrow.cpp b/plugin/src/Caelestia/Components/buttonrow.cpp new file mode 100644 index 00000000..6177ae4f --- /dev/null +++ b/plugin/src/Caelestia/Components/buttonrow.cpp @@ -0,0 +1,132 @@ +#include "buttonrow.hpp" + +namespace caelestia::components { + +ButtonRow::ButtonRow(QQuickItem* parent) + : QQuickItem(parent) + , m_dirty(false) + , m_spacing(0.0) { + setFlag(QQuickItem::ItemHasContents, true); + QObject::connect(this, &ButtonRow::widthChanged, this, &ButtonRow::invalidate); +} + +qreal ButtonRow::spacing() const { + return m_spacing; +} + +void ButtonRow::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing + 1.0, spacing + 1.0)) + return; + + m_spacing = spacing; + emit spacingChanged(); + + invalidate(); +} + +void ButtonRow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) { + if (change == QQuickItem::ItemChildAddedChange) { + auto* const child = data.item; + QObject::connect(child, &QQuickItem::implicitWidthChanged, this, &ButtonRow::invalidate); + QObject::connect(child, &QQuickItem::implicitHeightChanged, this, &ButtonRow::invalidate); + QObject::connect(child, &QQuickItem::visibleChanged, this, &ButtonRow::invalidate); + + const auto* childMeta = child->metaObject(); + const auto morphSignalIdx = childMeta->indexOfSignal("shapeMorphExpansionChanged()"); + if (morphSignalIdx != -1) + QObject::connect(child, childMeta->method(morphSignalIdx), this, + metaObject()->method(metaObject()->indexOfSlot("invalidate()"))); + + invalidate(); + } else if (change == QQuickItem::ItemChildRemovedChange) { + QObject::disconnect(data.item, nullptr, this, nullptr); + invalidate(); + } + + QQuickItem::itemChange(change, data); +} + +void ButtonRow::updatePolish() { + if (m_dirty) { + relayout(); + m_dirty = false; + } +} + +void ButtonRow::invalidate() { + m_dirty = true; + polish(); +} + +void ButtonRow::relayout() { + const auto allChildren = childItems(); + + QList validChildren; + for (auto* const child : allChildren) { + if (child->isVisible() && !child->inherits("QQuickRepeater")) + validChildren.append(child); + } + const auto nChildren = validChildren.size(); + const auto totalSpacing = static_cast(nChildren - 1) * m_spacing; + + qreal reservedWidth = 0; + qreal unreservedWidth = 0; + int fillWidthCount = 0; + qreal maxHeight = 0; + for (auto* const child : validChildren) { + maxHeight = qMax(maxHeight, child->implicitHeight()); + + const auto prop = child->property("fillWidth"); + if (!prop.isValid()) + continue; + + if (prop.toBool()) { + fillWidthCount++; + unreservedWidth += child->implicitWidth(); + } else { + reservedWidth += child->implicitWidth(); + } + } + + if (fillWidthCount == 0) + fillWidthCount = 1; // Avoid divide by 0 + + const auto widthPerItem = (width() - totalSpacing - reservedWidth) / static_cast(fillWidthCount); + + QList baseWidths; + baseWidths.reserve(nChildren); + for (auto* const child : validChildren) + baseWidths.append(child->property("fillWidth").toBool() ? widthPerItem : child->implicitWidth()); + + qreal accX = 0; + for (int i = 0; i < nChildren; ++i) { + auto* const child = validChildren[i]; + + // clang-format off + auto prevExtraWidth = i > 0 ? getMorphExpansion(validChildren[i - 1]) : 0.0; + auto nextExtraWidth = i < nChildren - 1 ? getMorphExpansion(validChildren[i + 1]) : 0.0; + // clang-format on + + // Items at edges push by full amount, items in middle push by half + if (i > 1) + prevExtraWidth /= 2; + if (i < nChildren - 2) + nextExtraWidth /= 2; + + child->setWidth(baseWidths[i] + getMorphExpansion(child) - prevExtraWidth - nextExtraWidth); + child->setHeight(maxHeight); + + child->setX(accX); + child->setY(0); + accX += child->width() + m_spacing; + } + + setImplicitWidth(reservedWidth + unreservedWidth + totalSpacing); + setImplicitHeight(maxHeight); +} + +qreal ButtonRow::getMorphExpansion(const QQuickItem* item) { + return item->property("shapeMorphExpansion").toReal(); +} + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/buttonrow.hpp b/plugin/src/Caelestia/Components/buttonrow.hpp new file mode 100644 index 00000000..1d138d8b --- /dev/null +++ b/plugin/src/Caelestia/Components/buttonrow.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace caelestia::components { + +class ButtonRow : public QQuickItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + +public: + explicit ButtonRow(QQuickItem* parent = nullptr); + + qreal spacing() const; + void setSpacing(qreal spacing); + +signals: + void spacingChanged(); + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) override; + void updatePolish() override; + +private slots: + void invalidate(); + +private: + void relayout(); + static qreal getMorphExpansion(const QQuickItem* item); + + bool m_dirty; + qreal m_spacing; +}; + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/wavyline.cpp b/plugin/src/Caelestia/Components/wavyline.cpp new file mode 100644 index 00000000..e61b896f --- /dev/null +++ b/plugin/src/Caelestia/Components/wavyline.cpp @@ -0,0 +1,255 @@ +#include "wavyline.hpp" + +#include +#include + +namespace caelestia::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 caelestia::controls diff --git a/plugin/src/Caelestia/Components/wavyline.hpp b/plugin/src/Caelestia/Components/wavyline.hpp new file mode 100644 index 00000000..d0f865b6 --- /dev/null +++ b/plugin/src/Caelestia/Components/wavyline.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +namespace caelestia::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 caelestia::controls diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 4b26ea02..b6f4ef08 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -6,6 +6,8 @@ qml_module(caelestia-config configobject.cpp rootconfig.cpp appearanceconfig.cpp + font.cpp + fontbuilder.cpp tokens.cpp tokensattached.cpp anim.cpp diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp index 59228c67..a5230135 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.cpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -36,14 +36,30 @@ int AppearanceRounding::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } -int AppearanceRounding::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +int AppearanceRounding::medium() const { + return m_tokens ? static_cast(m_tokens->medium() * m_scale) : 0; } int AppearanceRounding::large() const { return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; } +int AppearanceRounding::largeIncreased() const { + return m_tokens ? static_cast(m_tokens->largeIncreased() * m_scale) : 0; +} + +int AppearanceRounding::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +int AppearanceRounding::extraLargeIncreased() const { + return m_tokens ? static_cast(m_tokens->extraLargeIncreased() * m_scale) : 0; +} + +int AppearanceRounding::extraExtraLarge() const { + return m_tokens ? static_cast(m_tokens->extraExtraLarge() * m_scale) : 0; +} + int AppearanceRounding::full() const { return m_tokens ? static_cast(m_tokens->full() * m_scale) : 0; } @@ -55,26 +71,38 @@ void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { connectTokenSignals(tokens, this); } +int AppearanceSpacing::extraSmall() const { + return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; +} + int AppearanceSpacing::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } -int AppearanceSpacing::smaller() const { - return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -} - -int AppearanceSpacing::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -} - -int AppearanceSpacing::larger() const { - return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +int AppearanceSpacing::medium() const { + return m_tokens ? static_cast(m_tokens->medium() * m_scale) : 0; } int AppearanceSpacing::large() const { return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; } +int AppearanceSpacing::largeIncreased() const { + return m_tokens ? static_cast(m_tokens->largeIncreased() * m_scale) : 0; +} + +int AppearanceSpacing::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +int AppearanceSpacing::extraLargeIncreased() const { + return m_tokens ? static_cast(m_tokens->extraLargeIncreased() * m_scale) : 0; +} + +int AppearanceSpacing::extraExtraLarge() const { + return m_tokens ? static_cast(m_tokens->extraExtraLarge() * m_scale) : 0; +} + // AppearancePadding void AppearancePadding::bindTokens(PaddingTokens* tokens) { @@ -82,57 +110,52 @@ void AppearancePadding::bindTokens(PaddingTokens* tokens) { connectTokenSignals(tokens, this); } +int AppearancePadding::extraSmall() const { + return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; +} + int AppearancePadding::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } -int AppearancePadding::smaller() const { - return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -} - -int AppearancePadding::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -} - -int AppearancePadding::larger() const { - return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +int AppearancePadding::medium() const { + return m_tokens ? static_cast(m_tokens->medium() * m_scale) : 0; } int AppearancePadding::large() const { return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; } -// FontSize - -void FontSize::bindTokens(FontSizeTokens* tokens) { - m_tokens = tokens; - connectTokenSignals(tokens, this); +int AppearancePadding::largeIncreased() const { + return m_tokens ? static_cast(m_tokens->largeIncreased() * m_scale) : 0; } -int FontSize::small() const { - return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -} - -int FontSize::smaller() const { - return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -} - -int FontSize::normal() const { - return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -} - -int FontSize::larger() const { - return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; -} - -int FontSize::large() const { - return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -} - -int FontSize::extraLarge() const { +int AppearancePadding::extraLarge() const { return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; } +int AppearancePadding::extraLargeIncreased() const { + return m_tokens ? static_cast(m_tokens->extraLargeIncreased() * m_scale) : 0; +} + +int AppearancePadding::extraExtraLarge() const { + return m_tokens ? static_cast(m_tokens->extraExtraLarge() * m_scale) : 0; +} + +// FontConfig + +void FontConfig::setDefaults(int size, int weight, const QVariantMap& vaxes) { + m_size = size; + m_weight = weight; + m_vaxes = vaxes; +} + +// FontStyleConfig + +void FontStyleConfig::setDefaultFamily(const QString& family) { + m_family = family; +} + // AnimDurations void AnimDurations::bindTokens(AnimDurationTokens* tokens) { diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index 3446ea72..a5a95f3f 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -2,7 +2,9 @@ #include "configobject.hpp" +#include #include +#include namespace caelestia::config { @@ -10,7 +12,6 @@ namespace caelestia::config { class RoundingTokens; class SpacingTokens; class PaddingTokens; -class FontSizeTokens; class AnimDurationTokens; class AppearanceRounding : public ConfigObject { @@ -21,8 +22,12 @@ class AppearanceRounding : public ConfigObject { Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int medium READ medium NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged) Q_PROPERTY(int full READ full NOTIFY valuesChanged) public: @@ -33,8 +38,12 @@ public: [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; - [[nodiscard]] int normal() const; + [[nodiscard]] int medium() const; [[nodiscard]] int large() const; + [[nodiscard]] int largeIncreased() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int extraLargeIncreased() const; + [[nodiscard]] int extraExtraLarge() const; [[nodiscard]] int full() const; signals: @@ -50,11 +59,14 @@ class AppearanceSpacing : public ConfigObject { CONFIG_PROPERTY(qreal, scale, 1) + Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) - Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int medium READ medium NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged) public: explicit AppearanceSpacing(QObject* parent = nullptr) @@ -62,11 +74,14 @@ public: void bindTokens(SpacingTokens* tokens); + [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; - [[nodiscard]] int smaller() const; - [[nodiscard]] int normal() const; - [[nodiscard]] int larger() const; + [[nodiscard]] int medium() const; [[nodiscard]] int large() const; + [[nodiscard]] int largeIncreased() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int extraLargeIncreased() const; + [[nodiscard]] int extraExtraLarge() const; signals: void valuesChanged(); @@ -81,11 +96,14 @@ class AppearancePadding : public ConfigObject { CONFIG_PROPERTY(qreal, scale, 1) + Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) - Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int medium READ medium NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int largeIncreased READ largeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int extraLargeIncreased READ extraLargeIncreased NOTIFY valuesChanged) + Q_PROPERTY(int extraExtraLarge READ extraExtraLarge NOTIFY valuesChanged) public: explicit AppearancePadding(QObject* parent = nullptr) @@ -93,11 +111,14 @@ public: void bindTokens(PaddingTokens* tokens); + [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; - [[nodiscard]] int smaller() const; - [[nodiscard]] int normal() const; - [[nodiscard]] int larger() const; + [[nodiscard]] int medium() const; [[nodiscard]] int large() const; + [[nodiscard]] int largeIncreased() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int extraLargeIncreased() const; + [[nodiscard]] int extraExtraLarge() const; signals: void valuesChanged(); @@ -106,65 +127,113 @@ private: PaddingTokens* m_tokens = nullptr; }; -class FontFamily : public ConfigObject { +class FontConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) - CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) - CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) - CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) + // Empty family inherits from the parent FontStyleConfig. + CONFIG_PROPERTY(QString, family, {}) + CONFIG_PROPERTY(int, size, 14) + CONFIG_PROPERTY(int, weight, QFont::Normal) + CONFIG_PROPERTY(bool, italic, false) + CONFIG_PROPERTY(QVariantMap, vaxes, {}) public: - explicit FontFamily(QObject* parent = nullptr) + explicit FontConfig(QObject* parent = nullptr) : ConfigObject(parent) {} + + void setDefaults(int size, int weight = QFont::Normal, const QVariantMap& vaxes = {}); }; -class FontSize : public ConfigObject { +class FontStyleConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(qreal, scale, 1) - - Q_PROPERTY(int small READ small NOTIFY valuesChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) - Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) - Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) - Q_PROPERTY(int large READ large NOTIFY valuesChanged) - Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + CONFIG_PROPERTY(QString, family, QStringLiteral("GoogleSansFlex")) + CONFIG_SUBOBJECT(FontConfig, large) + CONFIG_SUBOBJECT(FontConfig, medium) + CONFIG_SUBOBJECT(FontConfig, small) public: - explicit FontSize(QObject* parent = nullptr) - : ConfigObject(parent) {} + explicit FontStyleConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_large(new FontConfig(this)) + , m_medium(new FontConfig(this)) + , m_small(new FontConfig(this)) {} - void bindTokens(FontSizeTokens* tokens); + void setDefaultFamily(const QString& family); +}; - [[nodiscard]] int small() const; - [[nodiscard]] int smaller() const; - [[nodiscard]] int normal() const; - [[nodiscard]] int larger() const; - [[nodiscard]] int large() const; - [[nodiscard]] int extraLarge() const; +class IconFontStyleConfig : public FontStyleConfig { + Q_OBJECT + QML_ANONYMOUS -signals: - void valuesChanged(); + CONFIG_SUBOBJECT(FontConfig, extraLarge) -private: - FontSizeTokens* m_tokens = nullptr; +public: + explicit IconFontStyleConfig(QObject* parent = nullptr) + : FontStyleConfig(parent) + , m_extraLarge(new FontConfig(this)) {} }; class AppearanceFont : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_SUBOBJECT(FontFamily, family) - CONFIG_SUBOBJECT(FontSize, size) + CONFIG_PROPERTY(qreal, scale, 1) + CONFIG_SUBOBJECT(FontStyleConfig, headline) + CONFIG_SUBOBJECT(FontStyleConfig, title) + CONFIG_SUBOBJECT(FontStyleConfig, body) + CONFIG_SUBOBJECT(FontStyleConfig, label) + CONFIG_SUBOBJECT(FontStyleConfig, mono) + CONFIG_SUBOBJECT(IconFontStyleConfig, icon) + CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) public: explicit AppearanceFont(QObject* parent = nullptr) : ConfigObject(parent) - , m_family(new FontFamily(this)) - , m_size(new FontSize(this)) {} + , m_headline(new FontStyleConfig(this)) + , m_title(new FontStyleConfig(this)) + , m_body(new FontStyleConfig(this)) + , m_label(new FontStyleConfig(this)) + , m_mono(new FontStyleConfig(this)) + , m_icon(new IconFontStyleConfig(this)) { + const auto sans = QStringLiteral("GoogleSansFlex"); + const auto mono = QStringLiteral("CaskaydiaCove NF"); + const auto icons = QStringLiteral("Material Symbols Rounded"); + const QVariantMap vaxes = { { "ROND", 25 } }; + + m_headline->setDefaultFamily(sans); + m_headline->large()->setDefaults(32, QFont::Medium, vaxes); + m_headline->medium()->setDefaults(28, QFont::Medium, vaxes); + m_headline->small()->setDefaults(24, QFont::Medium, vaxes); + + m_title->setDefaultFamily(sans); + m_title->large()->setDefaults(22, QFont::Medium, vaxes); + m_title->medium()->setDefaults(16, QFont::Medium, vaxes); + m_title->small()->setDefaults(14, QFont::Medium, vaxes); + + m_body->setDefaultFamily(sans); + m_body->large()->setDefaults(16, QFont::Normal, vaxes); + m_body->medium()->setDefaults(14, QFont::Normal, vaxes); + m_body->small()->setDefaults(12, QFont::Normal, vaxes); + + m_label->setDefaultFamily(sans); + m_label->large()->setDefaults(14, QFont::Medium, vaxes); + m_label->medium()->setDefaults(12, QFont::Medium, vaxes); + m_label->small()->setDefaults(11, QFont::Medium, vaxes); + + m_mono->setDefaultFamily(mono); + m_mono->large()->setDefaults(16, QFont::Normal); + m_mono->medium()->setDefaults(14, QFont::Normal); + m_mono->small()->setDefaults(12, QFont::Normal); + + m_icon->setDefaultFamily(icons); + m_icon->extraLarge()->setDefaults(static_cast(48 / 1.33), QFont::Normal); + m_icon->large()->setDefaults(static_cast(32 / 1.33), QFont::Normal); + m_icon->medium()->setDefaults(static_cast(24 / 1.33), QFont::Normal); + m_icon->small()->setDefaults(static_cast(20 / 1.33), QFont::Normal); + } }; class AnimDurations : public ConfigObject { diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index ff939f99..ecd63bb5 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -103,7 +103,6 @@ void GlobalConfig::bindAppearanceTokens() { m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); m_appearance->padding()->bindTokens(tokenAppearance->padding()); - m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); m_tokensBound = true; } diff --git a/plugin/src/Caelestia/Config/font.cpp b/plugin/src/Caelestia/Config/font.cpp new file mode 100644 index 00000000..edd75069 --- /dev/null +++ b/plugin/src/Caelestia/Config/font.cpp @@ -0,0 +1,263 @@ +#include "font.hpp" +#include "appearanceconfig.hpp" + +namespace caelestia::config { + +// FontStyleBase + +QFont FontStyleBase::large() const { + return m_large; +} + +QFont FontStyleBase::medium() const { + return m_medium; +} + +QFont FontStyleBase::small() const { + return m_small; +} + +QFont FontStyleBase::buildFont(const FontConfig* cfg, const QString& fallbackFamily, qreal scale) { + QFont font; + font.setFamily(cfg->family().isEmpty() ? fallbackFamily : cfg->family()); + const int scaledSize = static_cast(cfg->size() * scale); + const int cappedSize = scaledSize > 0 ? scaledSize : 1; + font.setPointSize(cappedSize); + font.setVariableAxis("opsz", static_cast(cappedSize)); + font.setWeight(QFont::Weight(cfg->weight())); + font.setVariableAxis("wght", font.weight()); + font.setItalic(cfg->italic()); + + const auto axes = cfg->vaxes(); + for (auto it = axes.constBegin(); it != axes.constEnd(); ++it) { + if (auto tag = QFont::Tag::fromString(it.key())) + font.setVariableAxis(*tag, it.value().toFloat()); + } + + return font; +} + +void FontStyleBase::bind(FontStyleConfig* cfg) { + if (m_cfg == cfg) + return; + + if (m_cfg) { + disconnect(m_cfg, nullptr, this, nullptr); + disconnect(m_cfg->large(), nullptr, this, nullptr); + disconnect(m_cfg->medium(), nullptr, this, nullptr); + disconnect(m_cfg->small(), nullptr, this, nullptr); + } + + m_cfg = cfg; + + if (cfg) { + connect(cfg, &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + connect(cfg->large(), &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + connect(cfg->medium(), &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + connect(cfg->small(), &ConfigObject::propertiesChanged, this, &FontStyleBase::rebuild); + } + + rebuild(); +} + +void FontStyleBase::rebuild() { + if (m_cfg) { + const auto family = m_cfg->family(); + m_large = buildFont(m_cfg->large(), family, m_scale); + m_medium = buildFont(m_cfg->medium(), family, m_scale); + m_small = buildFont(m_cfg->small(), family, m_scale); + } else { + m_large = QFont(); + m_medium = QFont(); + m_small = QFont(); + } + emit fontsChanged(); +} + +void FontStyleBase::setScale(qreal scale) { + if (qFuzzyCompare(m_scale + 1.0, scale + 1.0)) + return; + m_scale = scale; + rebuild(); +} + +// FontStyle + +FontStyle::FontStyle(QObject* parent) + : FontStyleBase(parent) + , m_builders(new FontBuilders(this, this)) {} + +FontBuilders* FontStyle::builders() const { + return m_builders; +} + +// IconFontStyle + +IconFontStyle::IconFontStyle(QObject* parent) + : FontStyleBase(parent) + , m_builders(new IconFontBuilders(this, this)) {} + +FontBuilder IconFontStyle::size(int pointSize) { + return FontBuilder(m_small).size(pointSize); +} + +void IconFontStyle::bind(IconFontStyleConfig* cfg) { + if (m_cfg == cfg) + return; + + if (m_cfg) { + // Previous cfg may or may not be an IconFontStyleConfig; guard via cast. + if (auto* prev = qobject_cast(m_cfg)) + disconnect(prev->extraLarge(), nullptr, this, nullptr); + } + + FontStyleBase::bind(cfg); + + if (cfg) + connect(cfg->extraLarge(), &ConfigObject::propertiesChanged, this, &IconFontStyle::rebuild); +} + +QFont IconFontStyle::extraLarge() const { + return m_extraLarge; +} + +IconFontBuilders* IconFontStyle::builders() const { + return m_builders; +} + +void IconFontStyle::rebuild() { + if (auto* cfg = qobject_cast(m_cfg)) { + const auto family = cfg->family(); + m_large = buildFont(cfg->large(), family, m_scale); + m_medium = buildFont(cfg->medium(), family, m_scale); + m_small = buildFont(cfg->small(), family, m_scale); + m_extraLarge = buildFont(cfg->extraLarge(), family, m_scale); + } else { + m_large = QFont(); + m_medium = QFont(); + m_small = QFont(); + m_extraLarge = QFont(); + } + emit fontsChanged(); +} + +// FontBuilders + +FontBuilders::FontBuilders(const FontStyleBase* style, QObject* parent) + : QObject(parent) + , m_style(style) { + connect(style, &FontStyleBase::fontsChanged, this, &FontBuilders::buildersChanged); +} + +FontBuilder FontBuilders::large() const { + return FontBuilder(m_style->large()); +} + +FontBuilder FontBuilders::medium() const { + return FontBuilder(m_style->medium()); +} + +FontBuilder FontBuilders::small() const { + return FontBuilder(m_style->small()); +} + +// IconFontBuilders + +IconFontBuilders::IconFontBuilders(const IconFontStyle* style, QObject* parent) + : FontBuilders(style, parent) {} + +FontBuilder IconFontBuilders::extraLarge() const { + return FontBuilder(static_cast(m_style)->extraLarge()); +} + +// FontTokens + +FontTokens::FontTokens(QObject* parent) + : QObject(parent) + , m_headline(new FontStyle(this)) + , m_title(new FontStyle(this)) + , m_body(new FontStyle(this)) + , m_label(new FontStyle(this)) + , m_mono(new FontStyle(this)) + , m_icon(new IconFontStyle(this)) {} + +FontStyle* FontTokens::headline() const { + return m_headline; +} + +FontStyle* FontTokens::title() const { + return m_title; +} + +FontStyle* FontTokens::body() const { + return m_body; +} + +FontStyle* FontTokens::label() const { + return m_label; +} + +FontStyle* FontTokens::mono() const { + return m_mono; +} + +IconFontStyle* FontTokens::icon() const { + return m_icon; +} + +FontBuilder FontTokens::clock() const { + return FontBuilder(m_clock); +} + +void FontTokens::bindFont(AppearanceFont* font) { + if (m_font == font) + return; + + if (m_font) + disconnect(m_font, nullptr, this, nullptr); + + m_font = font; + + if (font) { + rebuildScale(); + m_headline->bind(font->headline()); + m_title->bind(font->title()); + m_body->bind(font->body()); + m_label->bind(font->label()); + m_mono->bind(font->mono()); + m_icon->bind(font->icon()); + + connect(font, &AppearanceFont::clockChanged, this, &FontTokens::rebuildClock); + connect(font, &AppearanceFont::scaleChanged, this, &FontTokens::rebuildScale); + } else { + rebuildScale(); + m_headline->bind(nullptr); + m_title->bind(nullptr); + m_body->bind(nullptr); + m_label->bind(nullptr); + m_mono->bind(nullptr); + m_icon->bind(nullptr); + } + + rebuildClock(); +} + +void FontTokens::rebuildScale() { + const qreal s = m_font ? m_font->scale() : 1; + m_headline->setScale(s); + m_title->setScale(s); + m_body->setScale(s); + m_label->setScale(s); + m_mono->setScale(s); + m_icon->setScale(s); +} + +void FontTokens::rebuildClock() { + QFont f; + if (m_font) + f.setFamily(m_font->clock()); + m_clock = f; + emit clockChanged(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/font.hpp b/plugin/src/Caelestia/Config/font.hpp new file mode 100644 index 00000000..2cfbf341 --- /dev/null +++ b/plugin/src/Caelestia/Config/font.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include "fontbuilder.hpp" + +#include + +namespace caelestia::config { + +class AppearanceFont; +class FontConfig; +class FontStyleConfig; +class IconFontStyleConfig; +class FontStyleBase; +class IconFontStyle; + +class FontBuilders : public QObject { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontBuilder large READ large NOTIFY buildersChanged FINAL) + Q_PROPERTY(caelestia::config::FontBuilder medium READ medium NOTIFY buildersChanged FINAL) + Q_PROPERTY(caelestia::config::FontBuilder small READ small NOTIFY buildersChanged FINAL) + +public: + explicit FontBuilders(const FontStyleBase* style, QObject* parent = nullptr); + + [[nodiscard]] FontBuilder large() const; + [[nodiscard]] FontBuilder medium() const; + [[nodiscard]] FontBuilder small() const; + +signals: + void buildersChanged(); + +protected: + const FontStyleBase* m_style; +}; + +class IconFontBuilders : public FontBuilders { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontBuilder extraLarge READ extraLarge NOTIFY buildersChanged FINAL) + +public: + explicit IconFontBuilders(const IconFontStyle* style, QObject* parent = nullptr); + + [[nodiscard]] FontBuilder extraLarge() const; +}; + +class FontStyleBase : public QObject { + Q_OBJECT + + Q_PROPERTY(QFont large READ large NOTIFY fontsChanged FINAL) + Q_PROPERTY(QFont medium READ medium NOTIFY fontsChanged FINAL) + Q_PROPERTY(QFont small READ small NOTIFY fontsChanged FINAL) + +public: + explicit FontStyleBase(QObject* parent = nullptr) + : QObject(parent) {} + + void bind(FontStyleConfig* cfg); + void setScale(qreal scale); + + [[nodiscard]] QFont large() const; + [[nodiscard]] QFont medium() const; + [[nodiscard]] QFont small() const; + +signals: + void fontsChanged(); + +protected: + virtual void rebuild(); + + static QFont buildFont(const FontConfig* cfg, const QString& fallbackFamily, qreal scale); + + FontStyleConfig* m_cfg = nullptr; + qreal m_scale = 1; + QFont m_large; + QFont m_medium; + QFont m_small; +}; + +class FontStyle : public FontStyleBase { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontBuilders* builders READ builders CONSTANT FINAL) + +public: + explicit FontStyle(QObject* parent = nullptr); + + [[nodiscard]] FontBuilders* builders() const; + +private: + FontBuilders* m_builders; +}; + +class IconFontStyle : public FontStyleBase { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(QFont extraLarge READ extraLarge NOTIFY fontsChanged FINAL) + Q_PROPERTY(caelestia::config::IconFontBuilders* builders READ builders CONSTANT FINAL) + +public: + explicit IconFontStyle(QObject* parent = nullptr); + + Q_INVOKABLE FontBuilder size(int pointSize); + + void bind(IconFontStyleConfig* cfg); + + [[nodiscard]] QFont extraLarge() const; + [[nodiscard]] IconFontBuilders* builders() const; + +protected: + void rebuild() override; + +private: + QFont m_extraLarge; + IconFontBuilders* m_builders; +}; + +class FontTokens : public QObject { + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(caelestia::config::FontStyle* headline READ headline CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* title READ title CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* body READ body CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* label READ label CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontStyle* mono READ mono CONSTANT FINAL) + Q_PROPERTY(caelestia::config::IconFontStyle* icon READ icon CONSTANT FINAL) + Q_PROPERTY(caelestia::config::FontBuilder clock READ clock NOTIFY clockChanged FINAL) + +public: + explicit FontTokens(QObject* parent = nullptr); + + void bindFont(AppearanceFont* font); + + [[nodiscard]] FontStyle* headline() const; + [[nodiscard]] FontStyle* title() const; + [[nodiscard]] FontStyle* body() const; + [[nodiscard]] FontStyle* label() const; + [[nodiscard]] FontStyle* mono() const; + [[nodiscard]] IconFontStyle* icon() const; + [[nodiscard]] FontBuilder clock() const; + +signals: + void clockChanged(); + +private: + void rebuildClock(); + void rebuildScale(); + + AppearanceFont* m_font = nullptr; + FontStyle* m_headline; + FontStyle* m_title; + FontStyle* m_body; + FontStyle* m_label; + FontStyle* m_mono; + IconFontStyle* m_icon; + QFont m_clock; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/fontbuilder.cpp b/plugin/src/Caelestia/Config/fontbuilder.cpp new file mode 100644 index 00000000..2d2e3fd9 --- /dev/null +++ b/plugin/src/Caelestia/Config/fontbuilder.cpp @@ -0,0 +1,92 @@ +#include "fontbuilder.hpp" +#include + +namespace caelestia::config { + +Q_LOGGING_CATEGORY(lcFontBuilder, "caelestia.fontbuilder", QtInfoMsg) + +FontBuilder::FontBuilder(QFont font) + : m_font(std::move(font)) {} + +FontBuilder FontBuilder::family(const QString& family) { + m_font.setFamily(family); + return *this; +} + +FontBuilder FontBuilder::size(int pointSize) { + if (pointSize <= 0) + pointSize = 1; + m_font.setPointSize(pointSize); + m_font.setVariableAxis("opsz", static_cast(pointSize)); + return *this; +} + +FontBuilder FontBuilder::weight(QFont::Weight weight) { + m_font.setWeight(weight); + m_font.setVariableAxis("wght", weight); + return *this; +} + +FontBuilder FontBuilder::italic(bool on) { + m_font.setItalic(on); + return *this; +} + +FontBuilder FontBuilder::stretch(int stretch) { + m_font.setStretch(stretch); + return *this; +} + +FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) { + m_font.setLetterSpacing(absolute ? QFont::AbsoluteSpacing : QFont::PercentageSpacing, spacing); + return *this; +} + +FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) { + m_font.setCapitalization(cap); + return *this; +} + +FontBuilder FontBuilder::vaxis(const QString& tag, float value) { + if (auto t = QFont::Tag::fromString(tag)) + m_font.setVariableAxis(*t, value); + else + qCWarning(lcFontBuilder) << "Unable to convert tag" << tag << "to QFont::Tag"; + return *this; +} + +FontBuilder FontBuilder::vaxes(QVariantMap axes) { + for (auto it = axes.constBegin(); it != axes.constEnd(); ++it) { + if (it.value().canConvert()) { + if (auto tag = QFont::Tag::fromString(it.key())) + m_font.setVariableAxis(*tag, it.value().toFloat()); + else + qCWarning(lcFontBuilder) << "Unable to convert tag" << it.key() << "to QFont::Tag"; + } else { + qCWarning(lcFontBuilder) << "Unable to convert value" << it.value() << "to float"; + } + } + return *this; +} + +QFont FontBuilder::build() const { + return m_font; +} + +FontBuilder FontBuilder::fill(float value) { + return vaxis("FILL", value); +} + +FontBuilder FontBuilder::grade(float value) { + return vaxis("GRAD", value); +} + +FontBuilder FontBuilder::width(float value) { + return vaxis("wdth", value); +} + +FontBuilder FontBuilder::scale(qreal factor) { + return size(static_cast(m_font.pointSize() * factor)); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/fontbuilder.hpp b/plugin/src/Caelestia/Config/fontbuilder.hpp new file mode 100644 index 00000000..b159d08b --- /dev/null +++ b/plugin/src/Caelestia/Config/fontbuilder.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace caelestia::config { + +class FontBuilder { + Q_GADGET + QML_ANONYMOUS + +public: + FontBuilder() = default; + explicit FontBuilder(QFont font); + + [[nodiscard]] Q_INVOKABLE FontBuilder family(const QString& family); + [[nodiscard]] Q_INVOKABLE FontBuilder size(int pointSize); + [[nodiscard]] Q_INVOKABLE FontBuilder weight(QFont::Weight weight); + [[nodiscard]] Q_INVOKABLE FontBuilder italic(bool on = true); + [[nodiscard]] Q_INVOKABLE FontBuilder stretch(int stretch); + [[nodiscard]] Q_INVOKABLE FontBuilder letterSpacing(qreal spacing, bool absolute = true); + [[nodiscard]] Q_INVOKABLE FontBuilder capitalisation(QFont::Capitalization cap); + [[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value); + [[nodiscard]] Q_INVOKABLE FontBuilder vaxes(QVariantMap axes); + [[nodiscard]] Q_INVOKABLE QFont build() const; + + // Common vaxes + [[nodiscard]] Q_INVOKABLE FontBuilder fill(float value); + [[nodiscard]] Q_INVOKABLE FontBuilder grade(float value); + [[nodiscard]] Q_INVOKABLE FontBuilder width(float value); + + // Operations + [[nodiscard]] Q_INVOKABLE FontBuilder scale(qreal factor); + +private: + QFont m_font; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp index 0d8aa6ba..f44d398d 100644 --- a/plugin/src/Caelestia/Config/lockconfig.hpp +++ b/plugin/src/Caelestia/Config/lockconfig.hpp @@ -8,7 +8,7 @@ class LockConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, recolourLogo, false) + CONFIG_PROPERTY(bool, recolourLogo, true) CONFIG_GLOBAL_PROPERTY(bool, enableFprint, true) CONFIG_GLOBAL_PROPERTY(int, maxFprintTries, 3) CONFIG_PROPERTY(bool, hideNotifs, false) diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp index b97c69c1..60bbced1 100644 --- a/plugin/src/Caelestia/Config/serviceconfig.hpp +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -24,7 +24,7 @@ class ServiceConfig : public ConfigObject { CONFIG_GLOBAL_PROPERTY( bool, useTwelveHourClock, QLocale().timeFormat(QLocale::ShortFormat).toLower().contains(u"a"_s)) CONFIG_GLOBAL_PROPERTY(QString, gpuType) - CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) + CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 60) CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 28bac1f1..d29f62ee 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -2,6 +2,7 @@ #include "rootconfig.hpp" +#include #include #include @@ -46,10 +47,14 @@ class RoundingTokens : public ConfigObject { QML_ANONYMOUS CONFIG_PROPERTY(int, extraSmall, 4) - CONFIG_PROPERTY(int, small, 12) - CONFIG_PROPERTY(int, normal, 17) - CONFIG_PROPERTY(int, large, 25) - CONFIG_PROPERTY(int, full, 1000) + CONFIG_PROPERTY(int, small, 8) + CONFIG_PROPERTY(int, medium, 12) + CONFIG_PROPERTY(int, large, 16) + CONFIG_PROPERTY(int, largeIncreased, 20) + CONFIG_PROPERTY(int, extraLarge, 28) + CONFIG_PROPERTY(int, extraLargeIncreased, 32) + CONFIG_PROPERTY(int, extraExtraLarge, 48) + CONFIG_PROPERTY(int, full, std::numeric_limits::max()) public: explicit RoundingTokens(QObject* parent = nullptr) @@ -60,11 +65,14 @@ class SpacingTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, small, 7) - CONFIG_PROPERTY(int, smaller, 10) - CONFIG_PROPERTY(int, normal, 12) - CONFIG_PROPERTY(int, larger, 15) - CONFIG_PROPERTY(int, large, 20) + CONFIG_PROPERTY(int, extraSmall, 4) + CONFIG_PROPERTY(int, small, 8) + CONFIG_PROPERTY(int, medium, 12) + CONFIG_PROPERTY(int, large, 16) + CONFIG_PROPERTY(int, largeIncreased, 20) + CONFIG_PROPERTY(int, extraLarge, 28) + CONFIG_PROPERTY(int, extraLargeIncreased, 32) + CONFIG_PROPERTY(int, extraExtraLarge, 48) public: explicit SpacingTokens(QObject* parent = nullptr) @@ -75,11 +83,14 @@ class PaddingTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, small, 5) - CONFIG_PROPERTY(int, smaller, 7) - CONFIG_PROPERTY(int, normal, 10) - CONFIG_PROPERTY(int, larger, 12) - CONFIG_PROPERTY(int, large, 15) + CONFIG_PROPERTY(int, extraSmall, 4) + CONFIG_PROPERTY(int, small, 8) + CONFIG_PROPERTY(int, medium, 12) + CONFIG_PROPERTY(int, large, 16) + CONFIG_PROPERTY(int, largeIncreased, 20) + CONFIG_PROPERTY(int, extraLarge, 28) + CONFIG_PROPERTY(int, extraLargeIncreased, 32) + CONFIG_PROPERTY(int, extraExtraLarge, 48) public: explicit PaddingTokens(QObject* parent = nullptr) @@ -166,17 +177,28 @@ class DashboardTokens : public ConfigObject { CONFIG_PROPERTY(int, tabIndicatorHeight, 3) CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) - CONFIG_PROPERTY(int, infoWidth, 200) - CONFIG_PROPERTY(int, infoIconSize, 25) + CONFIG_PROPERTY(int, userWidth, 340) + CONFIG_PROPERTY(int, logoSize, 30) + CONFIG_PROPERTY(int, uptimeSize, 30) CONFIG_PROPERTY(int, dateTimeWidth, 110) CONFIG_PROPERTY(int, mediaWidth, 200) CONFIG_PROPERTY(int, mediaProgressSweep, 180) - CONFIG_PROPERTY(int, mediaProgressThickness, 8) - CONFIG_PROPERTY(int, resourceProgressThickness, 10) - CONFIG_PROPERTY(int, weatherWidth, 250) - CONFIG_PROPERTY(int, mediaCoverArtSize, 150) - CONFIG_PROPERTY(int, mediaVisualiserSize, 80) - CONFIG_PROPERTY(int, resourceSize, 200) + CONFIG_PROPERTY(int, mediaProgressThickness, 6) + CONFIG_PROPERTY(int, resourceProgressThickness, 6) + CONFIG_PROPERTY(int, weatherWidth, 275) + CONFIG_PROPERTY(int, mediaCoverArtSize, 200) + CONFIG_PROPERTY(int, mediaTabWidth, 1000) + CONFIG_PROPERTY(int, mediaTabHeight, 320) + CONFIG_PROPERTY(int, mediaSectionWidth, 300) + CONFIG_PROPERTY(int, perfHeroCardWidth, 400) + CONFIG_PROPERTY(int, perfUsageShapeSize, 100) + CONFIG_PROPERTY(int, perfStorageTextWidth, 160) + CONFIG_PROPERTY(int, perfNetworkCardWidth, 390) + CONFIG_PROPERTY(int, perfNetworkCardHeight, 220) + CONFIG_PROPERTY(int, perfBattWidth, 150) + CONFIG_PROPERTY(int, perfBattWidthSingle, 400) + CONFIG_PROPERTY(int, perfBattHeight, 160) + CONFIG_PROPERTY(int, perfPlaceholderWidth, 700) public: explicit DashboardTokens(QObject* parent = nullptr) @@ -201,8 +223,8 @@ class NotifsTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, width, 400) - CONFIG_GLOBAL_PROPERTY(int, image, 41) + CONFIG_PROPERTY(int, width, 430) + CONFIG_GLOBAL_PROPERTY(int, image, 42) CONFIG_PROPERTY(int, badge, 20) public: @@ -263,6 +285,14 @@ class LockTokens : public ConfigObject { CONFIG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) CONFIG_PROPERTY(int, centerWidth, 600) + CONFIG_PROPERTY(int, showWeatherDetailsHeight, 500) + CONFIG_PROPERTY(int, showForecastHeight, 975) + CONFIG_PROPERTY(int, forecastItemWidth, 51) + CONFIG_PROPERTY(int, largeLogoWidth, 320) + CONFIG_PROPERTY(int, largeFontWidth, 400) + CONFIG_PROPERTY(int, fetch4LinesHeight, 600) + CONFIG_PROPERTY(int, fetch3LinesHeight, 500) + CONFIG_PROPERTY(int, showColourBoxRowHeight, 570) public: explicit LockTokens(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 16f863b8..d7225981 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -2,6 +2,7 @@ #include "anim.hpp" #include "appearanceconfig.hpp" #include "config.hpp" +#include "font.hpp" #include "monitorconfigmanager.hpp" #include "tokens.hpp" @@ -23,8 +24,10 @@ const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, c Tokens::Tokens(QObject* parent) : QQuickAttachedPropertyPropagator(parent) + , m_font(new FontTokens(this)) , m_anim(new AnimTokens(this)) { bindAnim(); + bindFont(); initialize(); } @@ -52,6 +55,7 @@ void Tokens::inheritScreen(const QString& screen) { m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen); } + bindFont(); propagateScreen(); emit sourceChanged(); } @@ -78,6 +82,11 @@ void Tokens::bindAnim() { m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); } +void Tokens::bindFont() { + auto* appearance = m_config ? m_config->appearance() : GlobalConfig::instance()->appearance(); + m_font->bindFont(appearance->font()); +} + #define TOKENS_ATTACHED_GETTER(Type, name) \ const Type* Tokens::name() const { \ auto* a = resolveAppearance(m_config, m_complete, #name, parent()); \ @@ -87,7 +96,6 @@ void Tokens::bindAnim() { TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) TOKENS_ATTACHED_GETTER(AppearancePadding, padding) -TOKENS_ATTACHED_GETTER(AppearanceFont, font) #undef TOKENS_ATTACHED_GETTER @@ -103,6 +111,10 @@ const SizeTokens* Tokens::sizes() const { return TokenConfig::instance()->sizes(); } +const FontTokens* Tokens::font() const { + return m_font; +} + const AnimTokens* Tokens::anim() const { return m_anim; } diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp index 810b0c0d..623b2adc 100644 --- a/plugin/src/Caelestia/Config/tokensattached.hpp +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -7,11 +7,11 @@ namespace caelestia::config { class AnimTokens; -class AppearanceFont; class AppearancePadding; class AppearanceRounding; class AppearanceSpacing; class AppearanceTransparency; +class FontTokens; class GlobalConfig; class SizeTokens; class TokenConfig; @@ -24,15 +24,16 @@ class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus QML_ATTACHED(Tokens) Q_MOC_INCLUDE("anim.hpp") Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("font.hpp") Q_MOC_INCLUDE("tokens.hpp") Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::FontTokens* font READ font NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) public: @@ -44,10 +45,10 @@ public: [[nodiscard]] const AppearanceRounding* rounding() const; [[nodiscard]] const AppearanceSpacing* spacing() const; [[nodiscard]] const AppearancePadding* padding() const; - [[nodiscard]] const AppearanceFont* font() const; [[nodiscard]] const AppearanceTransparency* transparency() const; [[nodiscard]] const SizeTokens* sizes() const; + [[nodiscard]] const FontTokens* font() const; [[nodiscard]] const AnimTokens* anim() const; [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); @@ -67,11 +68,13 @@ private: void propagateScreen(); void bindAnim(); + void bindFont(); bool m_complete = false; QString m_screen; GlobalConfig* m_config = nullptr; TokenConfig* m_tokens = nullptr; + FontTokens* m_font = nullptr; AnimTokens* m_anim = nullptr; }; diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index f4bbc5fd..abcaf86b 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -1,9 +1,9 @@ qml_module(caelestia-internal URI Caelestia.Internal SOURCES - arcgauge.cpp circularbuffer.cpp circularindicatormanager.cpp + linearindicatormanager.cpp hyprdevices.cpp hyprextras.cpp logindmanager.cpp diff --git a/plugin/src/Caelestia/Internal/arcgauge.cpp b/plugin/src/Caelestia/Internal/arcgauge.cpp deleted file mode 100644 index d534f5f7..00000000 --- a/plugin/src/Caelestia/Internal/arcgauge.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "arcgauge.hpp" - -#include -#include -#include - -namespace caelestia::internal { - -ArcGauge::ArcGauge(QQuickItem* parent) - : QQuickPaintedItem(parent) { - setAntialiasing(true); -} - -void ArcGauge::paint(QPainter* painter) { - const qreal w = width(); - const qreal h = height(); - const qreal side = qMin(w, h); - const qreal radius = (side - m_lineWidth - 2.0) / 2.0; - const qreal cx = w / 2.0; - const qreal cy = h / 2.0; - - const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); - - // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) - const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); - const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); - - painter->setRenderHint(QPainter::Antialiasing, true); - - // Draw track arc - QPen trackPen(m_trackColor, m_lineWidth); - trackPen.setCapStyle(Qt::RoundCap); - painter->setPen(trackPen); - painter->setBrush(Qt::NoBrush); - painter->drawArc(arcRect, startAngle16, sweepAngle16); - - // Draw value arc - if (m_percentage > 0.0) { - const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); - QPen valuePen(m_accentColor, m_lineWidth); - valuePen.setCapStyle(Qt::RoundCap); - painter->setPen(valuePen); - painter->drawArc(arcRect, startAngle16, valueSweep16); - } -} - -qreal ArcGauge::percentage() const { - return m_percentage; -} - -void ArcGauge::setPercentage(qreal percentage) { - if (qFuzzyCompare(m_percentage, percentage)) - return; - m_percentage = percentage; - emit percentageChanged(); - update(); -} - -QColor ArcGauge::accentColor() const { - return m_accentColor; -} - -void ArcGauge::setAccentColor(const QColor& color) { - if (m_accentColor == color) - return; - m_accentColor = color; - emit accentColorChanged(); - update(); -} - -QColor ArcGauge::trackColor() const { - return m_trackColor; -} - -void ArcGauge::setTrackColor(const QColor& color) { - if (m_trackColor == color) - return; - m_trackColor = color; - emit trackColorChanged(); - update(); -} - -qreal ArcGauge::startAngle() const { - return m_startAngle; -} - -void ArcGauge::setStartAngle(qreal angle) { - if (qFuzzyCompare(m_startAngle, angle)) - return; - m_startAngle = angle; - emit startAngleChanged(); - update(); -} - -qreal ArcGauge::sweepAngle() const { - return m_sweepAngle; -} - -void ArcGauge::setSweepAngle(qreal angle) { - if (qFuzzyCompare(m_sweepAngle, angle)) - return; - m_sweepAngle = angle; - emit sweepAngleChanged(); - update(); -} - -qreal ArcGauge::lineWidth() const { - return m_lineWidth; -} - -void ArcGauge::setLineWidth(qreal width) { - if (qFuzzyCompare(m_lineWidth, width)) - return; - m_lineWidth = width; - emit lineWidthChanged(); - update(); -} - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/arcgauge.hpp b/plugin/src/Caelestia/Internal/arcgauge.hpp deleted file mode 100644 index 4ccb1fd0..00000000 --- a/plugin/src/Caelestia/Internal/arcgauge.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace caelestia::internal { - -class ArcGauge : public QQuickPaintedItem { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) - Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) - Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) - Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) - Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) - Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) - -public: - explicit ArcGauge(QQuickItem* parent = nullptr); - - void paint(QPainter* painter) override; - - [[nodiscard]] qreal percentage() const; - void setPercentage(qreal percentage); - - [[nodiscard]] QColor accentColor() const; - void setAccentColor(const QColor& color); - - [[nodiscard]] QColor trackColor() const; - void setTrackColor(const QColor& color); - - [[nodiscard]] qreal startAngle() const; - void setStartAngle(qreal angle); - - [[nodiscard]] qreal sweepAngle() const; - void setSweepAngle(qreal angle); - - [[nodiscard]] qreal lineWidth() const; - void setLineWidth(qreal width); - -signals: - void percentageChanged(); - void accentColorChanged(); - void trackColorChanged(); - void startAngleChanged(); - void sweepAngleChanged(); - void lineWidthChanged(); - -private: - qreal m_percentage = 0.0; - QColor m_accentColor; - QColor m_trackColor; - qreal m_startAngle = 0.75 * M_PI; - qreal m_sweepAngle = 1.5 * M_PI; - qreal m_lineWidth = 10.0; -}; - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/linearindicatormanager.cpp b/plugin/src/Caelestia/Internal/linearindicatormanager.cpp new file mode 100644 index 00000000..597a593d --- /dev/null +++ b/plugin/src/Caelestia/Internal/linearindicatormanager.cpp @@ -0,0 +1,119 @@ +#include "linearindicatormanager.hpp" + +#include + +namespace { + +// See +// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/progressindicator/LinearIndeterminateDisjointAnimatorDelegate.java#L44-L46 +constexpr int TOTAL_DURATION_IN_MS = 1800; +constexpr std::array DURATION_TO_MOVE_SEGMENT_ENDS = { 533, 567, 850, 750 }; +constexpr std::array DELAY_TO_MOVE_SEGMENT_ENDS = { 1267, 1000, 333, 0 }; + +QEasingCurve curve(const QPointF& c1, const QPointF& c2) { + QEasingCurve curve(QEasingCurve::BezierSpline); + curve.addCubicBezierSegment(c1, c2, { 1.0, 1.0 }); + return curve; +} + +qreal getFractionInRange(qreal playtime, int start, int duration) { + const auto fraction = static_cast(playtime - start) / duration; + return std::clamp(fraction, 0.0, 1.0); +} + +} // namespace + +namespace caelestia::controls { + +LinearIndicatorSegment::LinearIndicatorSegment(int gap, QObject* parent) + : QObject(parent) + , m_startFraction(0) + , m_endFraction(0) + , m_gapSize(gap) {} + +qreal LinearIndicatorSegment::startFraction() const { + return m_startFraction; +} + +qreal LinearIndicatorSegment::endFraction() const { + return m_endFraction; +} + +int LinearIndicatorSegment::gapSize() const { + return m_gapSize; +} + +LinearIndicatorManager::LinearIndicatorManager(QObject* parent) + : QObject(parent) + , m_interpolators({ + curve({ 0.2, 0.0 }, { 0.8, 1.0 }), + curve({ 0.4, 0.0 }, { 1.0, 1.0 }), + curve({ 0.0, 0.0 }, { 0.65, 1.0 }), + curve({ 0.1, 0.0 }, { 0.45, 1.0 }), + }) + , m_progress(0) + , m_completeEndProgress(0) + , m_gap(4) + , m_activeIndicators({ + new LinearIndicatorSegment(m_gap, this), + new LinearIndicatorSegment(m_gap, this), + }) { + for (auto el : m_activeIndicators) + QObject::connect(this, &LinearIndicatorManager::updated, el, &LinearIndicatorSegment::updated); +} + +QList LinearIndicatorManager::activeIndicators() const { + return { m_activeIndicators.cbegin(), m_activeIndicators.cend() }; +} + +qreal LinearIndicatorManager::progress() const { + return m_progress; +} + +qreal LinearIndicatorManager::completeEndProgress() const { + return m_completeEndProgress; +} + +int LinearIndicatorManager::gap() const { + return m_gap; +} + +void LinearIndicatorManager::setGap(int gap) { + m_gap = gap; + for (auto el : m_activeIndicators) + el->m_gapSize = m_gap; + update(m_progress); +} + +int LinearIndicatorManager::duration() const { + return TOTAL_DURATION_IN_MS; +} + +int LinearIndicatorManager::completeEndDuration() const { + return TOTAL_DURATION_IN_MS; +} + +void LinearIndicatorManager::update(qreal progress) { + const auto playtime = progress * TOTAL_DURATION_IN_MS; + for (size_t i = 0; i < SEGMENTS; i++) { + const auto di = i * 2; + auto* const indicator = m_activeIndicators[i]; + + auto fraction = getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di], DURATION_TO_MOVE_SEGMENT_ENDS[di]); + indicator->m_startFraction = std::clamp(m_interpolators[di].valueForProgress(fraction), 0.0, 1.0); + + fraction = + getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di + 1], DURATION_TO_MOVE_SEGMENT_ENDS[di + 1]); + indicator->m_endFraction = std::clamp(m_interpolators[di + 1].valueForProgress(fraction), 0.0, 1.0); + } + + m_progress = progress; + emit updated(); +} + +void LinearIndicatorManager::updateCompleteEndProgress(qreal progress) { + m_completeEndProgress = progress; + update(m_progress); +} + +} // namespace caelestia::controls diff --git a/plugin/src/Caelestia/Internal/linearindicatormanager.hpp b/plugin/src/Caelestia/Internal/linearindicatormanager.hpp new file mode 100644 index 00000000..272e8643 --- /dev/null +++ b/plugin/src/Caelestia/Internal/linearindicatormanager.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::controls { + +class LinearIndicatorManager; + +class LinearIndicatorSegment : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("LinearIndicatorSegments can only be retrieved from a " + "LinearIndicatorManager.") + + Q_PROPERTY(qreal startFraction READ startFraction NOTIFY updated FINAL) + Q_PROPERTY(qreal endFraction READ endFraction NOTIFY updated FINAL) + Q_PROPERTY(int gapSize READ gapSize NOTIFY updated FINAL) + +public: + explicit LinearIndicatorSegment(int gap, QObject* parent = nullptr); + + qreal startFraction() const; + qreal endFraction() const; + int gapSize() const; + +signals: + void updated(); + +private: + qreal m_startFraction; + qreal m_endFraction; + int m_gapSize; + + friend LinearIndicatorManager; +}; + +class LinearIndicatorManager : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY( + QList activeIndicators READ activeIndicators CONSTANT FINAL) + + Q_PROPERTY(qreal progress READ progress WRITE update NOTIFY updated FINAL) + Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE updateCompleteEndProgress NOTIFY updated FINAL) + Q_PROPERTY(int gap READ gap WRITE setGap NOTIFY updated FINAL) + + Q_PROPERTY(qreal duration READ duration CONSTANT FINAL) + Q_PROPERTY(qreal completeEndDuration READ completeEndDuration CONSTANT FINAL) + +public: + explicit LinearIndicatorManager(QObject* parent = nullptr); + + QList activeIndicators() const; + + qreal progress() const; + qreal completeEndProgress() const; + + int gap() const; + void setGap(int gap); + + int duration() const; + int completeEndDuration() const; + + void update(qreal progress); + void updateCompleteEndProgress(qreal progress); + +signals: + void updated(); + +private: + static constexpr int SEGMENTS = 2; + + std::array m_interpolators; + qreal m_progress; + qreal m_completeEndProgress; + int m_gap; + + std::array m_activeIndicators; +}; + +} // namespace caelestia::controls diff --git a/plugin/src/Caelestia/Services/CMakeLists.txt b/plugin/src/Caelestia/Services/CMakeLists.txt index 8ce868b5..5c85ae5f 100644 --- a/plugin/src/Caelestia/Services/CMakeLists.txt +++ b/plugin/src/Caelestia/Services/CMakeLists.txt @@ -1,14 +1,27 @@ qml_module(caelestia-services URI Caelestia.Services SOURCES - service.hpp service.cpp - serviceref.hpp serviceref.cpp - beattracker.hpp beattracker.cpp - audiocollector.hpp audiocollector.cpp - audioprovider.hpp audioprovider.cpp - cavaprovider.hpp cavaprovider.cpp + service.cpp + serviceref.cpp + beattracker.cpp + audiocollector.cpp + audioprovider.cpp + cavaprovider.cpp + tickingservice.cpp + sensorslib.cpp + usagefmt.cpp + cpu.cpp + gpu.cpp + memory.cpp + diskinfo.cpp + storage.cpp + lyriccandidate.cpp + lyrics.cpp LIBRARIES PkgConfig::Pipewire PkgConfig::Aubio PkgConfig::Cava + Sensors::Sensors + caelestia-config + caelestia-internal ) diff --git a/plugin/src/Caelestia/Services/cpu.cpp b/plugin/src/Caelestia/Services/cpu.cpp new file mode 100644 index 00000000..2872508a --- /dev/null +++ b/plugin/src/Caelestia/Services/cpu.cpp @@ -0,0 +1,117 @@ +#include "cpu.hpp" + +#include "sensorslib.hpp" + +#include +#include +#include + +namespace caelestia::services { + +Cpu::Cpu(QObject* parent) + : TickingService(parent) { + readNameOnce(); +} + +QString Cpu::name() const { + return m_name; +} + +qreal Cpu::percentage() const { + return m_percentage; +} + +qreal Cpu::temperature() const { + return m_temperature; +} + +void Cpu::tick() { + if (!m_nameLoaded) { + readNameOnce(); + } + refreshPercentage(); + refreshTemperature(); +} + +void Cpu::readNameOnce() { + QFile f(QStringLiteral("/proc/cpuinfo")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + const QByteArray data = f.readAll(); + f.close(); + + static const QRegularExpression re(QStringLiteral("model name\\s*:\\s*(.+)")); + const auto match = re.match(QString::fromLatin1(data)); + if (!match.hasMatch()) { + return; + } + + const QString cleaned = cleanName(match.captured(1)); + m_nameLoaded = true; + if (cleaned == m_name) { + return; + } + m_name = cleaned; + Q_EMIT nameChanged(); +} + +void Cpu::refreshPercentage() { + QFile f(QStringLiteral("/proc/stat")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + const QByteArray data = f.readAll(); + f.close(); + + static const QRegularExpression re( + QStringLiteral("^cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)")); + const auto match = re.match(QString::fromLatin1(data)); + if (!match.hasMatch()) { + return; + } + + quint64 total = 0; + quint64 idle = 0; + for (int i = 1; i <= 7; ++i) { + const quint64 v = match.captured(i).toULongLong(); + total += v; + if (i == 4 || i == 5) { + idle += v; + } + } + + const quint64 totalDiff = total > m_lastTotal ? total - m_lastTotal : 0; + const quint64 idleDiff = idle > m_lastIdle ? idle - m_lastIdle : 0; + const qreal newPerc = totalDiff > 0 ? 1.0 - static_cast(idleDiff) / static_cast(totalDiff) : 0.0; + + m_lastTotal = total; + m_lastIdle = idle; + + if (std::abs(newPerc - m_percentage) > 0.0001) { + m_percentage = newPerc; + Q_EMIT percentageChanged(); + } +} + +void Cpu::refreshTemperature() { + const auto t = sensorslib::cpuPackageTemp(); + const qreal newTemp = t.value_or(0.0); + if (std::abs(newTemp - m_temperature) > 0.05) { + m_temperature = newTemp; + Q_EMIT temperatureChanged(); + } +} + +QString Cpu::cleanName(QString s) { + static const QRegularExpression noise( + QStringLiteral("\\(R\\)|\\(TM\\)|CPU|\\d+(?:th|nd|rd|st) Gen |Core |Processor"), + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression spaces(QStringLiteral("\\s+")); + + s.replace(noise, QString()); + s.replace(spaces, QStringLiteral(" ")); + return s.trimmed(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/cpu.hpp b/plugin/src/Caelestia/Services/cpu.hpp new file mode 100644 index 00000000..72db08ff --- /dev/null +++ b/plugin/src/Caelestia/Services/cpu.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "tickingservice.hpp" + +#include + +namespace caelestia::services { + +class Cpu : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) + Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged) + +public: + explicit Cpu(QObject* parent = nullptr); + + [[nodiscard]] QString name() const; + [[nodiscard]] qreal percentage() const; + [[nodiscard]] qreal temperature() const; + +signals: + void nameChanged(); + void percentageChanged(); + void temperatureChanged(); + +protected: + void tick() override; + +private: + void readNameOnce(); + void refreshPercentage(); + void refreshTemperature(); + + [[nodiscard]] static QString cleanName(QString s); + + QString m_name; + qreal m_percentage = 0.0; + qreal m_temperature = 0.0; + quint64 m_lastIdle = 0; + quint64 m_lastTotal = 0; + bool m_nameLoaded = false; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/diskinfo.cpp b/plugin/src/Caelestia/Services/diskinfo.cpp new file mode 100644 index 00000000..7da2a6cb --- /dev/null +++ b/plugin/src/Caelestia/Services/diskinfo.cpp @@ -0,0 +1,67 @@ +#include "diskinfo.hpp" + +namespace caelestia::services { + +namespace { + +constexpr qreal kKib = 1024.0; + +} // namespace + +DiskInfo::DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent) + : QObject(parent) + , m_mount(std::move(mount)) + , m_usedBytes(usedBytes) + , m_totalBytes(totalBytes) + , m_hasRoot(hasRoot) {} + +QString DiskInfo::mount() const { + return m_mount; +} + +qreal DiskInfo::used() const { + return static_cast(m_usedBytes) / kKib; +} + +qreal DiskInfo::total() const { + return static_cast(m_totalBytes) / kKib; +} + +qreal DiskInfo::free() const { + const quint64 freeBytes = m_totalBytes > m_usedBytes ? m_totalBytes - m_usedBytes : 0; + return static_cast(freeBytes) / kKib; +} + +qreal DiskInfo::perc() const { + return m_totalBytes > 0 ? static_cast(m_usedBytes) / static_cast(m_totalBytes) : 0.0; +} + +bool DiskInfo::hasRoot() const { + return m_hasRoot; +} + +void DiskInfo::update(quint64 usedBytes, quint64 totalBytes, bool hasRoot) { + const bool usedDiff = usedBytes != m_usedBytes; + const bool totalDiff = totalBytes != m_totalBytes; + const bool rootDiff = hasRoot != m_hasRoot; + + m_usedBytes = usedBytes; + m_totalBytes = totalBytes; + m_hasRoot = hasRoot; + + if (usedDiff) { + Q_EMIT usedChanged(); + } + if (totalDiff) { + Q_EMIT totalChanged(); + } + if (usedDiff || totalDiff) { + Q_EMIT freeChanged(); + Q_EMIT percChanged(); + } + if (rootDiff) { + Q_EMIT hasRootChanged(); + } +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/diskinfo.hpp b/plugin/src/Caelestia/Services/diskinfo.hpp new file mode 100644 index 00000000..331d87b6 --- /dev/null +++ b/plugin/src/Caelestia/Services/diskinfo.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +namespace caelestia::services { + +class DiskInfo : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("DiskInfo is created by DiskUsage") + + Q_PROPERTY(QString mount READ mount CONSTANT) + Q_PROPERTY(qreal used READ used NOTIFY usedChanged) + Q_PROPERTY(qreal total READ total NOTIFY totalChanged) + Q_PROPERTY(qreal free READ free NOTIFY freeChanged) + Q_PROPERTY(qreal perc READ perc NOTIFY percChanged) + Q_PROPERTY(bool hasRoot READ hasRoot NOTIFY hasRootChanged) + +public: + DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent = nullptr); + + [[nodiscard]] QString mount() const; + [[nodiscard]] qreal used() const; // KiB + [[nodiscard]] qreal total() const; // KiB + [[nodiscard]] qreal free() const; // KiB + [[nodiscard]] qreal perc() const; + [[nodiscard]] bool hasRoot() const; + + void update(quint64 usedBytes, quint64 totalBytes, bool hasRoot); + +signals: + void usedChanged(); + void totalChanged(); + void freeChanged(); + void percChanged(); + void hasRootChanged(); + +private: + QString m_mount; + quint64 m_usedBytes; + quint64 m_totalBytes; + bool m_hasRoot; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/gpu.cpp b/plugin/src/Caelestia/Services/gpu.cpp new file mode 100644 index 00000000..c9b7fa3e --- /dev/null +++ b/plugin/src/Caelestia/Services/gpu.cpp @@ -0,0 +1,256 @@ +#include "gpu.hpp" + +#include "../Config/config.hpp" +#include "../Config/serviceconfig.hpp" +#include "sensorslib.hpp" + +#include +#include +#include +#include + +namespace caelestia::services { + +namespace { + +constexpr const char* kTypeDetectScript = + "if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then echo NVIDIA;" + " elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC;" + " else echo NONE; fi"; + +constexpr const char* kNameDetectScript = "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null" + " || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1" + " || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"; + +} // namespace + +Gpu::Gpu(QObject* parent) + : TickingService(parent) { + auto* svc = caelestia::config::GlobalConfig::instance()->services(); + m_userType = parseType(svc->gpuType()); + QObject::connect(svc, &caelestia::config::ServiceConfig::gpuTypeChanged, this, [this, svc] { + setUserType(parseType(svc->gpuType())); + }); + + // Detection must run before any ServiceRef appears: callers may gate the ref on + // `type !== Gpu.None`, which would otherwise deadlock the detection. + if (m_userType == Auto) { + detectTypeOnce(); + } + detectNameOnce(); +} + +Gpu::Type Gpu::type() const { + return m_userType == Auto ? m_autoType : m_userType; +} + +Gpu::Type Gpu::userType() const { + return m_userType; +} + +Gpu::Type Gpu::autoType() const { + return m_autoType; +} + +QString Gpu::name() const { + return m_name; +} + +qreal Gpu::percentage() const { + return m_percentage; +} + +qreal Gpu::temperature() const { + return m_temperature; +} + +void Gpu::setUserType(Type value) { + if (value == m_userType) { + return; + } + const Type prevDerived = type(); + m_userType = value; + Q_EMIT userTypeChanged(); + if (type() != prevDerived) { + Q_EMIT typeChanged(); + } +} + +void Gpu::setAutoType(Type value) { + if (value == m_autoType) { + return; + } + const Type prevDerived = type(); + m_autoType = value; + Q_EMIT autoTypeChanged(); + if (type() != prevDerived) { + Q_EMIT typeChanged(); + } +} + +void Gpu::setName(QString value) { + if (value == m_name) { + return; + } + m_name = std::move(value); + Q_EMIT nameChanged(); +} + +void Gpu::tick() { + const Type t = type(); + if (t == Generic) { + readGenericUsage(); + readGpuTemperature(); + } else if (t == Nvidia) { + startNvidiaUsage(); + } else { + if (std::abs(m_percentage) > 0.0001) { + m_percentage = 0.0; + Q_EMIT percentageChanged(); + } + if (std::abs(m_temperature) > 0.05) { + m_temperature = 0.0; + Q_EMIT temperatureChanged(); + } + } +} + +void Gpu::detectTypeOnce() { + if (m_typeProc) { + return; + } + m_typeProc = new QProcess(this); + QObject::connect(m_typeProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QByteArray out = m_typeProc->readAllStandardOutput().trimmed(); + if (!out.isEmpty()) { + setAutoType(parseType(QString::fromLatin1(out))); + } + m_typeProc->deleteLater(); + m_typeProc = nullptr; + }); + m_typeProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript) }); +} + +void Gpu::detectNameOnce() { + if (m_nameProc) { + return; + } + m_nameProc = new QProcess(this); + QObject::connect(m_nameProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QString output = QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed(); + if (!output.isEmpty()) { + const QString lower = output.toLower(); + if (lower.contains(QStringLiteral("nvidia")) || lower.contains(QStringLiteral("geforce")) || + lower.contains(QStringLiteral("rtx")) || lower.contains(QStringLiteral("gtx")) || + lower.contains(QStringLiteral("rx"))) { + setName(cleanName(output)); + } else { + static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$")); + const auto bracket = bracketRe.match(output); + if (bracket.hasMatch()) { + setName(cleanName(bracket.captured(1))); + } else { + static const QRegularExpression colonRe(QStringLiteral(":\\s*(.+)")); + const auto colon = colonRe.match(output); + if (colon.hasMatch()) { + setName(cleanName(colon.captured(1))); + } + } + } + } + m_nameProc->deleteLater(); + m_nameProc = nullptr; + }); + m_nameProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript) }); +} + +void Gpu::readGenericUsage() { + const QStringList paths = + QDir(QStringLiteral("/sys/class/drm")) + .entryList(QStringList() << QStringLiteral("card*"), QDir::Dirs | QDir::NoDotAndDotDot); + qreal sum = 0.0; + int count = 0; + for (const QString& card : paths) { + QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent").arg(card)); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + continue; + } + bool ok = false; + const qreal v = f.readAll().trimmed().toDouble(&ok); + f.close(); + if (ok) { + sum += v; + ++count; + } + } + const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0; + if (std::abs(newPerc - m_percentage) > 0.0001) { + m_percentage = newPerc; + Q_EMIT percentageChanged(); + } +} + +void Gpu::startNvidiaUsage() { + if (m_nvidiaProc) { + return; + } + m_nvidiaProc = new QProcess(this); + QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QByteArray out = m_nvidiaProc->readAllStandardOutput().trimmed(); + m_nvidiaProc->deleteLater(); + m_nvidiaProc = nullptr; + + const QList parts = out.split(','); + if (parts.size() < 2) { + return; + } + bool ok1 = false; + bool ok2 = false; + const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0; + const qreal temp = parts.at(1).trimmed().toDouble(&ok2); + if (ok1 && std::abs(usage - m_percentage) > 0.0001) { + m_percentage = usage; + Q_EMIT percentageChanged(); + } + if (ok2 && std::abs(temp - m_temperature) > 0.05) { + m_temperature = temp; + Q_EMIT temperatureChanged(); + } + }); + m_nvidiaProc->start(QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu"), + QStringLiteral("--format=csv,noheader,nounits") }); +} + +void Gpu::readGpuTemperature() { + const auto t = sensorslib::gpuPciAverageTemp(); + const qreal newTemp = t.value_or(0.0); + if (std::abs(newTemp - m_temperature) > 0.05) { + m_temperature = newTemp; + Q_EMIT temperatureChanged(); + } +} + +Gpu::Type Gpu::parseType(const QString& s) { + const QString u = s.trimmed().toUpper(); + if (u.isEmpty()) { + return Auto; + } + if (u == QStringLiteral("NVIDIA")) { + return Nvidia; + } + if (u == QStringLiteral("GENERIC")) { + return Generic; + } + return None; +} + +QString Gpu::cleanName(QString s) { + static const QRegularExpression noise( + QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression spaces(QStringLiteral("\\s+")); + s.replace(noise, QString()); + s.replace(spaces, QStringLiteral(" ")); + return s.trimmed(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/gpu.hpp b/plugin/src/Caelestia/Services/gpu.hpp new file mode 100644 index 00000000..7111e554 --- /dev/null +++ b/plugin/src/Caelestia/Services/gpu.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "tickingservice.hpp" + +#include +#include + +namespace caelestia::services { + +class Gpu : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + enum Type { + Auto, // user override is empty (config "") — defer to detected autoType + None, // no usable GPU + Nvidia, // queried via nvidia-smi + Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent + }; + Q_ENUM(Type) + +private: + Q_PROPERTY(Type type READ type NOTIFY typeChanged) + Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged) + Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) + Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged) + +public: + explicit Gpu(QObject* parent = nullptr); + + [[nodiscard]] Type type() const; + [[nodiscard]] Type userType() const; + [[nodiscard]] Type autoType() const; + [[nodiscard]] QString name() const; + [[nodiscard]] qreal percentage() const; + [[nodiscard]] qreal temperature() const; + +signals: + void typeChanged(); + void userTypeChanged(); + void autoTypeChanged(); + void nameChanged(); + void percentageChanged(); + void temperatureChanged(); + +protected: + void tick() override; + +private: + void detectTypeOnce(); + void detectNameOnce(); + void readGenericUsage(); + void startNvidiaUsage(); + void readGpuTemperature(); + + void setUserType(Type value); + void setAutoType(Type value); + void setName(QString value); + + [[nodiscard]] static Type parseType(const QString& s); + [[nodiscard]] static QString cleanName(QString s); + + Type m_userType = Auto; + Type m_autoType = None; + QString m_name; + qreal m_percentage = 0.0; + qreal m_temperature = 0.0; + + QProcess* m_typeProc = nullptr; + QProcess* m_nameProc = nullptr; + QProcess* m_nvidiaProc = nullptr; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyriccandidate.cpp b/plugin/src/Caelestia/Services/lyriccandidate.cpp new file mode 100644 index 00000000..4d18c123 --- /dev/null +++ b/plugin/src/Caelestia/Services/lyriccandidate.cpp @@ -0,0 +1,65 @@ +#include "lyriccandidate.hpp" + +namespace caelestia::services { + +QString LyricsBackend::toString(Backend b) { + switch (b) { + case LyricsBackend::Auto: + return QStringLiteral("Auto"); + case LyricsBackend::Local: + return QStringLiteral("Local"); + case LyricsBackend::LRCLIB: + return QStringLiteral("LRCLIB"); + case LyricsBackend::NetEase: + return QStringLiteral("NetEase"); + default: + return QStringLiteral("Unknown"); + } +} + +LyricCandidate::LyricCandidate( + LyricsBackend::Backend backend, QString id, QString title, QString artist, QString album, qreal duration) + : m_backend(backend) + , m_id(std::move(id)) + , m_title(std::move(title)) + , m_artist(std::move(artist)) + , m_album(std::move(album)) + , m_duration(duration) {} + +LyricsBackend::Backend LyricCandidate::backend() const { + return m_backend; +} + +QString LyricCandidate::id() const { + return m_id; +} + +QString LyricCandidate::title() const { + return m_title; +} + +QString LyricCandidate::artist() const { + return m_artist; +} + +QString LyricCandidate::album() const { + return m_album; +} + +qreal LyricCandidate::duration() const { + return m_duration; +} + +bool LyricCandidate::isValid() const { + return !m_id.isEmpty(); +} + +bool LyricCandidate::operator==(const LyricCandidate& o) const noexcept { + return m_backend == o.m_backend && m_id == o.m_id; +} + +bool LyricCandidate::operator!=(const LyricCandidate& o) const noexcept { + return !(*this == o); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyriccandidate.hpp b/plugin/src/Caelestia/Services/lyriccandidate.hpp new file mode 100644 index 00000000..7d728306 --- /dev/null +++ b/plugin/src/Caelestia/Services/lyriccandidate.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +namespace caelestia::services { + +class LyricsBackend : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + enum Backend { + Auto = 0, + Local, + LRCLIB, + NetEase + }; + Q_ENUM(Backend) + + Q_INVOKABLE QString toString(caelestia::services::LyricsBackend::Backend b); +}; + +class LyricCandidate { + Q_GADGET + QML_VALUE_TYPE(lyricCandidate) + + Q_PROPERTY(caelestia::services::LyricsBackend::Backend backend READ backend) + Q_PROPERTY(QString id READ id) + Q_PROPERTY(QString title READ title) + Q_PROPERTY(QString artist READ artist) + Q_PROPERTY(QString album READ album) + Q_PROPERTY(qreal duration READ duration) + +public: + LyricCandidate() = default; + LyricCandidate(LyricsBackend::Backend backend, QString id, QString title, QString artist, QString album = {}, + qreal duration = 0.0); + + [[nodiscard]] LyricsBackend::Backend backend() const; + [[nodiscard]] QString id() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QString artist() const; + [[nodiscard]] QString album() const; + [[nodiscard]] qreal duration() const; + + [[nodiscard]] bool isValid() const; + bool operator==(const LyricCandidate& o) const noexcept; + bool operator!=(const LyricCandidate& o) const noexcept; + +private: + LyricsBackend::Backend m_backend = LyricsBackend::Auto; + QString m_id; + QString m_title; + QString m_artist; + QString m_album; + qreal m_duration = 0.0; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyrics.cpp b/plugin/src/Caelestia/Services/lyrics.cpp new file mode 100644 index 00000000..7490b7ac --- /dev/null +++ b/plugin/src/Caelestia/Services/lyrics.cpp @@ -0,0 +1,1048 @@ +#include "lyrics.hpp" + +#include "../Config/config.hpp" +#include "../Config/serviceconfig.hpp" +#include "../Config/userpaths.hpp" + +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcLyrics, "caelestia.lyrics", QtInfoMsg) + +namespace caelestia::services { + +using Qt::StringLiterals::operator""_s; +using Qt::StringLiterals::operator""_ba; + +namespace { + +constexpr int kLoadDebounceMs = 50; +constexpr qreal kIndexFudge = 0.1; + +[[nodiscard]] const QHash& netEaseHeaders() { + static const QHash h = { + { "User-Agent"_ba, "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"_ba }, + { "Referer"_ba, "https://music.163.com/"_ba }, + }; + return h; +} + +[[nodiscard]] const QHash& lrclibHeaders() { + static const QHash h = { + { "User-Agent"_ba, "caelestia-shell (https://github.com/caelestia-dots/shell)"_ba }, + }; + return h; +} + +[[nodiscard]] QString joinArtists(const QString& s) { + return s.trimmed(); +} + +[[nodiscard]] QString sanitizeFilenamePart(const QString& s) { + QString out; + out.reserve(s.size()); + for (const QChar c : s) { + if (c == QLatin1Char('/') || c == QLatin1Char('\0')) { + out.append(QLatin1Char('_')); + } else { + out.append(c); + } + } + return out; +} + +[[nodiscard]] bool containsCi(const QString& haystack, const QString& needle) { + return haystack.contains(needle, Qt::CaseInsensitive); +} + +} // namespace + +Lyrics::Lyrics(QObject* parent) + : QObject(parent) + , m_nam(new QNetworkAccessManager(this)) + , m_loadDebounce(new QTimer(this)) { + m_loadDebounce->setSingleShot(true); + m_loadDebounce->setInterval(kLoadDebounceMs); + QObject::connect(m_loadDebounce, &QTimer::timeout, this, &Lyrics::doLoad); + + const auto* cfg = config::GlobalConfig::instance(); + const auto* svcCfg = cfg->services(); + const auto* paths = cfg->paths(); + + m_preferredBackend = backendFromKey(svcCfg->lyricsBackend()); + + QObject::connect( + svcCfg, &config::ServiceConfig::lyricsBackendChanged, this, &Lyrics::onPreferredBackendConfigChanged); + QObject::connect(paths, &config::UserPaths::lyricsDirChanged, this, &Lyrics::onLyricsDirChanged); + + loadLyricsMap(); +} + +QStringList Lyrics::lyrics() const { + return m_lyrics; +} + +LyricsBackend::Backend Lyrics::backend() const { + return m_backend; +} + +LyricsBackend::Backend Lyrics::preferredBackend() const { + return m_preferredBackend; +} + +void Lyrics::setPreferredBackend(LyricsBackend::Backend value) { + if (m_preferredBackend == value) { + return; + } + m_preferredBackend = value; + emit preferredBackendChanged(); + + auto* const svcCfg = config::GlobalConfig::instance()->services(); + const QString key = backendKey(value); + if (svcCfg->lyricsBackend() != key) { + svcCfg->set_lyricsBackend(key); + } + + scheduleLoad(); +} + +QList Lyrics::lyricCandidates() const { + return m_candidates; +} + +LyricCandidate Lyrics::selectedCandidate() const { + return m_selected; +} + +void Lyrics::setSelectedCandidate(const LyricCandidate& value) { + if (m_selected == value) { + return; + } + m_selected = value; + emit selectedCandidateChanged(); + + if (!value.isValid()) { + return; + } + + const auto b = value.backend(); + setBackend(b); + setLoading(true); + + cancelInFlight(); + const int reqId = newRequestId(); + + if (b == LyricsBackend::LRCLIB || b == LyricsBackend::NetEase) { + const QString cached = readCachedLrc(b, value.id()); + if (!cached.isEmpty()) { + const auto lines = parseLrc(cached); + if (!lines.isEmpty()) { + setLines(lines, b); + setLoading(false); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + return; + } + } + } + + if (b == LyricsBackend::LRCLIB) { + fetchLrclibById(value.id(), reqId); + } else if (b == LyricsBackend::NetEase) { + fetchNetEaseLyricsById(value.id(), reqId); + } else if (b == LyricsBackend::Local) { + // For local, the id is the file path. Read directly. + QFile f(value.id()); + if (f.open(QIODevice::ReadOnly)) { + const QString text = QString::fromUtf8(f.readAll()); + setLines(parseLrc(text), LyricsBackend::Local); + setLoading(false); + } else { + qCWarning(lcLyrics) << "selectedCandidate: cannot open local file" << value.id(); + setLoading(false); + } + } + + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } +} + +bool Lyrics::loading() const { + return m_loading; +} + +bool Lyrics::hasLyrics() const { + return m_hasLyrics; +} + +qreal Lyrics::offset() const { + return m_offset; +} + +void Lyrics::setOffset(qreal value) { + if (qFuzzyCompare(m_offset, value)) { + return; + } + m_offset = value; + emit offsetChanged(); + + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } +} + +QString Lyrics::trackArtist() const { + return m_artist; +} + +QString Lyrics::trackTitle() const { + return m_title; +} + +int Lyrics::indexForTime(qreal time) const { + if (m_lines.isEmpty()) { + return -1; + } + const qreal target = time - m_offset + kIndexFudge; + qsizetype lo = 0; + qsizetype hi = m_lines.size(); + while (lo < hi) { + const qsizetype mid = lo + (hi - lo) / 2; + if (m_lines.at(mid).time <= target) { + lo = mid + 1; + } else { + hi = mid; + } + } + return static_cast(lo - 1); +} + +void Lyrics::setTrack(const QString& artist, const QString& title, const QString& album, qreal duration) { + const QString a = artist.trimmed(); + const QString t = title.trimmed(); + + if (a == m_artist && t == m_title && album == m_album && qFuzzyCompare(duration + 1.0, m_duration + 1.0)) { + return; + } + + m_artist = a; + m_title = t; + m_album = album; + m_duration = duration; + emit trackChanged(); + + scheduleLoad(); +} + +void Lyrics::clearTrack() { + cancelInFlight(); + m_artist.clear(); + m_title.clear(); + m_album.clear(); + m_duration = 0.0; + emit trackChanged(); + + clearCandidates(); + clearLines(); + setLoading(false); +} + +void Lyrics::refresh() { + scheduleLoad(); +} + +void Lyrics::setBackend(LyricsBackend::Backend value) { + if (m_backend == value) { + return; + } + m_backend = value; + emit backendChanged(); +} + +void Lyrics::setLoading(bool value) { + if (m_loading == value) { + return; + } + m_loading = value; + emit loadingChanged(); +} + +void Lyrics::setLines(QVector lines, LyricsBackend::Backend source) { + std::sort(lines.begin(), lines.end(), [](const LyricLine& a, const LyricLine& b) { + return a.time < b.time; + }); + + m_lines = std::move(lines); + QStringList list; + list.reserve(m_lines.size()); + for (const auto& l : std::as_const(m_lines)) { + list.append(l.text); + } + m_lyrics = std::move(list); + + setBackend(source); + emit lyricsChanged(); + + const auto hasLyrics = !m_lines.isEmpty(); + if (hasLyrics != m_hasLyrics) { + m_hasLyrics = hasLyrics; + emit hasLyricsChanged(); + } +} + +void Lyrics::clearLines() { + // Doesn't actually clear lines, set a flag instead so anims can run + m_hasLyrics = false; + emit hasLyricsChanged(); +} + +void Lyrics::appendCandidates(const QList& add) { + if (add.isEmpty()) { + return; + } + bool changed = false; + for (const auto& c : add) { + if (!m_candidates.contains(c)) { + m_candidates.append(c); + changed = true; + } + } + if (changed) { + emit lyricCandidatesChanged(); + } +} + +void Lyrics::clearCandidates() { + if (m_candidates.isEmpty()) { + return; + } + m_candidates.clear(); + emit lyricCandidatesChanged(); +} + +void Lyrics::scheduleLoad() { + m_loadDebounce->start(); +} + +int Lyrics::newRequestId() { + return ++m_currentRequestId; +} + +void Lyrics::cancelInFlight() { + for (auto it = m_pendingReplies.begin(); it != m_pendingReplies.end(); ++it) { + for (auto& ptr : it.value()) { + if (auto* reply = ptr.data()) { + reply->abort(); + reply->deleteLater(); + } + } + } + m_pendingReplies.clear(); +} + +void Lyrics::trackReply(int reqId, QNetworkReply* reply) { + if (!reply) { + return; + } + m_pendingReplies[reqId].append(QPointer(reply)); +} + +void Lyrics::doLoad() { + if (m_artist.isEmpty() && m_title.isEmpty()) { + clearLines(); + clearCandidates(); + setLoading(false); + return; + } + + cancelInFlight(); + const int reqId = newRequestId(); + + setLoading(true); + clearLines(); + clearCandidates(); + + // Restore per-track prefs (offset, last-selected backend/id) + m_settingFromPrefs = true; + const QJsonObject saved = m_lyricsMap.value(trackKey()).toObject(); + setOffset(saved.value(u"offset"_s).toDouble(0.0)); + LyricCandidate restored; + const QString savedBackendKey = saved.value(u"backend"_s).toString(); + const QString savedId = saved.value(u"id"_s).toString(); + if (!savedBackendKey.isEmpty() && !savedId.isEmpty()) { + restored = LyricCandidate(backendFromKey(savedBackendKey), savedId, m_title, m_artist, m_album, m_duration); + } + m_settingFromPrefs = false; + + // Always populate online candidates for the picker, regardless of preferred backend + searchLrclibCandidates(reqId); + searchNetEaseCandidates(reqId); + + if (restored.isValid()) { + // Honor saved selection for this track + m_settingFromPrefs = true; + setSelectedCandidate(restored); + m_settingFromPrefs = false; + return; + } + + // Primary attempt by preferred backend + switch (m_preferredBackend) { + case LyricsBackend::Local: + tryLocal(reqId); + break; + case LyricsBackend::LRCLIB: + tryLrclib(reqId); + break; + case LyricsBackend::NetEase: + tryNetEase(reqId); + break; + case LyricsBackend::Auto: + default: + tryLocal(reqId); + break; + } +} + +void Lyrics::chainNext(LyricsBackend::Backend just_failed, int reqId) { + if (m_preferredBackend != LyricsBackend::Auto) { + // Non-auto modes don't chain + setLoading(false); + return; + } + switch (just_failed) { + case LyricsBackend::Local: + tryLrclib(reqId); + return; + case LyricsBackend::LRCLIB: + tryNetEase(reqId); + return; + case LyricsBackend::NetEase: + default: + setLoading(false); + return; + } +} + +void Lyrics::tryLocal(int reqId) { + if (reqId != m_currentRequestId) { + return; + } + + setBackend(LyricsBackend::Local); + + const QString dir = lyricsDir(); + if (dir.isEmpty()) { + chainNext(LyricsBackend::Local, reqId); + return; + } + + const QString direct = tryReadLocalLrc(dir, m_artist, m_title); + if (!direct.isEmpty()) { + QFile f(direct); + if (f.open(QIODevice::ReadOnly)) { + const QString text = QString::fromUtf8(f.readAll()); + const auto lines = parseLrc(text); + if (!lines.isEmpty()) { + setLines(lines, LyricsBackend::Local); + appendCandidates( + { LyricCandidate(LyricsBackend::Local, direct, m_title, m_artist, m_album, m_duration) }); + m_selected = LyricCandidate(LyricsBackend::Local, direct, m_title, m_artist, m_album, m_duration); + emit selectedCandidateChanged(); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + setLoading(false); + return; + } + } + } + + const QString recursive = findLocalLrcRecursive(dir, m_artist, m_title); + if (!recursive.isEmpty()) { + QFile f(recursive); + if (f.open(QIODevice::ReadOnly)) { + const QString text = QString::fromUtf8(f.readAll()); + const auto lines = parseLrc(text); + if (!lines.isEmpty()) { + setLines(lines, LyricsBackend::Local); + appendCandidates( + { LyricCandidate(LyricsBackend::Local, recursive, m_title, m_artist, m_album, m_duration) }); + m_selected = LyricCandidate(LyricsBackend::Local, recursive, m_title, m_artist, m_album, m_duration); + emit selectedCandidateChanged(); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + setLoading(false); + return; + } + } + } + + qCDebug(lcLyrics) << "no local lrc for" << m_artist << "-" << m_title; + chainNext(LyricsBackend::Local, reqId); +} + +void Lyrics::tryLrclib(int reqId) { + if (reqId != m_currentRequestId) { + return; + } + + setBackend(LyricsBackend::LRCLIB); + + QUrl url(u"https://lrclib.net/api/get"_s); + QUrlQuery q; + q.addQueryItem(u"track_name"_s, m_title); + q.addQueryItem(u"artist_name"_s, m_artist); + if (!m_album.isEmpty()) { + q.addQueryItem(u"album_name"_s, m_album); + } + if (m_duration > 0) { + q.addQueryItem(u"duration"_s, QString::number(qRound(m_duration))); + } + url.setQuery(q); + + auto* reply = getJson(url, lrclibHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "lrclib /get error:" << reply->errorString(); + chainNext(LyricsBackend::LRCLIB, reqId); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonObject obj = doc.object(); + const QString synced = obj.value(u"syncedLyrics"_s).toString(); + const qint64 id = static_cast(obj.value(u"id"_s).toDouble()); + + if (synced.isEmpty()) { + qCDebug(lcLyrics) << "lrclib: no syncedLyrics for" << m_artist << "-" << m_title; + chainNext(LyricsBackend::LRCLIB, reqId); + return; + } + + const auto lines = parseLrc(synced); + if (lines.isEmpty()) { + chainNext(LyricsBackend::LRCLIB, reqId); + return; + } + + writeCachedLrc(LyricsBackend::LRCLIB, QString::number(id), synced); + setLines(lines, LyricsBackend::LRCLIB); + const LyricCandidate cand(LyricsBackend::LRCLIB, QString::number(id), obj.value(u"trackName"_s).toString(), + obj.value(u"artistName"_s).toString(), obj.value(u"albumName"_s).toString(), + obj.value(u"duration"_s).toDouble()); + appendCandidates({ cand }); + m_selected = cand; + emit selectedCandidateChanged(); + if (!m_settingFromPrefs) { + persistTrackPrefs(); + } + setLoading(false); + }); +} + +void Lyrics::tryNetEase(int reqId) { + if (reqId != m_currentRequestId) { + return; + } + + setBackend(LyricsBackend::NetEase); + + // Reset cookies (LyricsBackend::NetEase rejects requests with stale cookies sometimes) + m_nam->setCookieJar(new QNetworkCookieJar(m_nam)); + + QUrl url(u"https://music.163.com/api/search/get"_s); + QUrlQuery q; + q.addQueryItem(u"s"_s, u"%1 %2"_s.arg(m_title, m_artist)); + q.addQueryItem(u"type"_s, u"1"_s); + q.addQueryItem(u"limit"_s, u"5"_s); + url.setQuery(q); + + auto* reply = getJson(url, netEaseHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "netease /search error:" << reply->errorString(); + chainNext(LyricsBackend::NetEase, reqId); + return; + } + + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonArray songs = doc.object().value(u"result"_s).toObject().value(u"songs"_s).toArray(); + + // Find best match by artist substring + qint64 bestId = -1; + for (const auto& v : songs) { + const QJsonObject s = v.toObject(); + const QJsonArray artists = s.value(u"artists"_s).toArray(); + if (artists.isEmpty()) { + continue; + } + const QString sArtist = artists.first().toObject().value(u"name"_s).toString(); + if (containsCi(m_artist, sArtist) || containsCi(sArtist, m_artist)) { + bestId = static_cast(s.value(u"id"_s).toDouble()); + break; + } + } + + if (bestId < 0) { + qCDebug(lcLyrics) << "netease: no artist match for" << m_artist << "-" << m_title; + chainNext(LyricsBackend::NetEase, reqId); + return; + } + + fetchNetEaseLyricsById(QString::number(bestId), reqId); + }); +} + +void Lyrics::searchLrclibCandidates(int reqId) { + QUrl url(u"https://lrclib.net/api/search"_s); + QUrlQuery q; + q.addQueryItem(u"track_name"_s, m_title); + q.addQueryItem(u"artist_name"_s, m_artist); + url.setQuery(q); + + auto* reply = getJson(url, lrclibHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "lrclib /search error:" << reply->errorString(); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonArray arr = doc.array(); + + QList add; + add.reserve(arr.size()); + for (const auto& v : arr) { + const QJsonObject o = v.toObject(); + if (o.value(u"syncedLyrics"_s).isNull() && o.value(u"plainLyrics"_s).isNull()) { + continue; + } + add.append( + LyricCandidate(LyricsBackend::LRCLIB, QString::number(static_cast(o.value(u"id"_s).toDouble())), + o.value(u"trackName"_s).toString(), o.value(u"artistName"_s).toString(), + o.value(u"albumName"_s).toString(), o.value(u"duration"_s).toDouble())); + } + appendCandidates(add); + }); +} + +void Lyrics::searchNetEaseCandidates(int reqId) { + m_nam->setCookieJar(new QNetworkCookieJar(m_nam)); + + QUrl url(u"https://music.163.com/api/search/get"_s); + QUrlQuery q; + q.addQueryItem(u"s"_s, u"%1 %2"_s.arg(m_title, m_artist)); + q.addQueryItem(u"type"_s, u"1"_s); + q.addQueryItem(u"limit"_s, u"5"_s); + url.setQuery(q); + + auto* reply = getJson(url, netEaseHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCDebug(lcLyrics) << "netease candidates error:" << reply->errorString(); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonArray songs = doc.object().value(u"result"_s).toObject().value(u"songs"_s).toArray(); + + QList add; + add.reserve(songs.size()); + for (const auto& v : songs) { + const QJsonObject s = v.toObject(); + QStringList artistNames; + const QJsonArray artists = s.value(u"artists"_s).toArray(); + artistNames.reserve(artists.size()); + for (const auto& a : artists) { + artistNames.append(a.toObject().value(u"name"_s).toString()); + } + add.append(LyricCandidate(LyricsBackend::NetEase, + QString::number(static_cast(s.value(u"id"_s).toDouble())), s.value(u"name"_s).toString(), + artistNames.join(u", "_s))); + } + appendCandidates(add); + }); +} + +void Lyrics::fetchLrclibById(const QString& id, int reqId) { + QUrl url(u"https://lrclib.net/api/get/"_s + id); + auto* reply = getJson(url, lrclibHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId, id] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCWarning(lcLyrics) << "lrclib /get/{id} error:" << reply->errorString(); + setLoading(false); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QString synced = doc.object().value(u"syncedLyrics"_s).toString(); + if (synced.isEmpty()) { + qCDebug(lcLyrics) << "lrclib /get/{id}: no syncedLyrics"; + setLoading(false); + return; + } + writeCachedLrc(LyricsBackend::LRCLIB, id, synced); + setLines(parseLrc(synced), LyricsBackend::LRCLIB); + setLoading(false); + }); +} + +void Lyrics::fetchNetEaseLyricsById(const QString& id, int reqId) { + QUrl url(u"https://music.163.com/api/song/lyric"_s); + QUrlQuery q; + q.addQueryItem(u"id"_s, id); + q.addQueryItem(u"lv"_s, u"1"_s); + q.addQueryItem(u"kv"_s, u"1"_s); + q.addQueryItem(u"tv"_s, u"-1"_s); + url.setQuery(q); + + auto* reply = getJson(url, netEaseHeaders()); + trackReply(reqId, reply); + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId, id] { + reply->deleteLater(); + if (reqId != m_currentRequestId) { + return; + } + if (reply->error() != QNetworkReply::NoError) { + qCWarning(lcLyrics) << "netease /lyric error:" << reply->errorString(); + setLoading(false); + return; + } + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + const QString lrc = doc.object().value(u"lrc"_s).toObject().value(u"lyric"_s).toString(); + if (lrc.isEmpty()) { + qCDebug(lcLyrics) << "netease /lyric: empty for id" << id; + setLoading(false); + return; + } + writeCachedLrc(LyricsBackend::NetEase, id, lrc); + setLines(parseLrc(lrc), LyricsBackend::NetEase); + setLoading(false); + }); +} + +QNetworkReply* Lyrics::getJson(const QUrl& url, const QHash& headers) { + QNetworkRequest req(url); + req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + req.setRawHeader("Cache-Control"_ba, "no-cache, no-store"_ba); + req.setRawHeader("Pragma"_ba, "no-cache"_ba); + req.setRawHeader("Connection"_ba, "close"_ba); + req.setRawHeader("Accept"_ba, "application/json"_ba); + for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) { + req.setRawHeader(it.key(), it.value()); + } + return m_nam->get(req); +} + +void Lyrics::onPreferredBackendConfigChanged() { + auto* svcCfg = config::GlobalConfig::instance()->services(); + const LyricsBackend::Backend desired = backendFromKey(svcCfg->lyricsBackend()); + if (desired == m_preferredBackend) { + return; + } + m_preferredBackend = desired; + emit preferredBackendChanged(); + scheduleLoad(); +} + +void Lyrics::onLyricsDirChanged() { + loadLyricsMap(); + scheduleLoad(); +} + +void Lyrics::loadLyricsMap() { + m_lyricsMap = {}; + m_lyricsMapLoaded = false; + + QFile f(lyricsMapPath()); + if (!f.open(QIODevice::ReadOnly)) { + m_lyricsMapLoaded = true; + return; + } + const QByteArray bytes = f.readAll(); + f.close(); + + QJsonParseError err{}; + const QJsonDocument doc = QJsonDocument::fromJson(bytes, &err); + if (err.error != QJsonParseError::NoError) { + qCWarning(lcLyrics) << "lyrics_map.json parse error:" << err.errorString(); + m_lyricsMapLoaded = true; + return; + } + m_lyricsMap = doc.object(); + m_lyricsMapLoaded = true; +} + +void Lyrics::persistTrackPrefs() { + if (!m_lyricsMapLoaded || trackKey().isEmpty()) { + return; + } + const QString key = trackKey(); + QJsonObject entry = m_lyricsMap.value(key).toObject(); + entry.insert(u"offset"_s, m_offset); + if (m_selected.isValid()) { + entry.insert(u"backend"_s, backendKey(m_selected.backend())); + entry.insert(u"id"_s, m_selected.id()); + } + m_lyricsMap.insert(key, entry); + + const QString dir = lyricsDir(); + if (dir.isEmpty()) { + return; + } + QDir().mkpath(dir); + + QSaveFile out(lyricsMapPath()); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(lcLyrics) << "cannot open" << lyricsMapPath() << "for write:" << out.errorString(); + return; + } + const QByteArray bytes = QJsonDocument(m_lyricsMap).toJson(QJsonDocument::Compact); + if (out.write(bytes) != bytes.size()) { + qCWarning(lcLyrics) << "short write to" << lyricsMapPath(); + out.cancelWriting(); + return; + } + if (!out.commit()) { + qCWarning(lcLyrics) << "commit failed for" << lyricsMapPath() << ":" << out.errorString(); + } +} + +QString Lyrics::lyricsDir() const { + QString dir = config::GlobalConfig::instance()->paths()->lyricsDir(); + if (dir.isEmpty()) { + return {}; + } + while (dir.endsWith(QLatin1Char('/')) && dir.size() > 1) { + dir.chop(1); + } + return dir; +} + +QString Lyrics::lyricsMapPath() const { + const QString dir = lyricsDir(); + return dir.isEmpty() ? QString() : dir + u"/lyrics_map.json"_s; +} + +QString Lyrics::trackKey() const { + if (m_artist.isEmpty() && m_title.isEmpty()) { + return {}; + } + return u"%1 - %2"_s.arg(joinArtists(m_artist), m_title); +} + +QString Lyrics::backendKey(LyricsBackend::Backend value) { + switch (value) { + case LyricsBackend::Local: + return u"LyricsBackend::Local"_s; + case LyricsBackend::LRCLIB: + return u"LyricsBackend::LRCLIB"_s; + case LyricsBackend::NetEase: + return u"LyricsBackend::NetEase"_s; + case LyricsBackend::Auto: + default: + return u"LyricsBackend::Auto"_s; + } +} + +LyricsBackend::Backend Lyrics::backendFromKey(const QString& key) { + if (key.compare(u"LyricsBackend::Local"_s, Qt::CaseInsensitive) == 0) { + return LyricsBackend::Local; + } + if (key.compare(u"LyricsBackend::LRCLIB"_s, Qt::CaseInsensitive) == 0) { + return LyricsBackend::LRCLIB; + } + if (key.compare(u"LyricsBackend::NetEase"_s, Qt::CaseInsensitive) == 0) { + return LyricsBackend::NetEase; + } + return LyricsBackend::Auto; +} + +const QString& Lyrics::cacheDir() { + static const QString s_dir = [] { + QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); + if (cache.isEmpty()) { + cache = QDir::homePath() + u"/.cache"_s; + } + return cache + u"/caelestia/lyrics"_s; + }(); + return s_dir; +} + +QString Lyrics::cachePathFor(LyricsBackend::Backend backend, const QString& id) { + if (id.isEmpty() || backend == LyricsBackend::Auto || backend == LyricsBackend::Local) { + return {}; + } + return u"%1/%2/%3.lrc"_s.arg(cacheDir(), backendKey(backend), sanitizeFilenamePart(id)); +} + +QString Lyrics::readCachedLrc(LyricsBackend::Backend backend, const QString& id) { + const QString path = cachePathFor(backend, id); + if (path.isEmpty()) { + return {}; + } + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + return {}; + } + return QString::fromUtf8(f.readAll()); +} + +void Lyrics::writeCachedLrc(LyricsBackend::Backend backend, const QString& id, const QString& text) { + if (text.isEmpty()) { + return; + } + const QString path = cachePathFor(backend, id); + if (path.isEmpty()) { + return; + } + QDir().mkpath(QFileInfo(path).absolutePath()); + + QSaveFile out(path); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(lcLyrics) << "cannot open" << path << "for write:" << out.errorString(); + return; + } + const QByteArray bytes = text.toUtf8(); + if (out.write(bytes) != bytes.size()) { + qCWarning(lcLyrics) << "short write to" << path; + out.cancelWriting(); + return; + } + if (!out.commit()) { + qCWarning(lcLyrics) << "commit failed for" << path << ":" << out.errorString(); + } +} + +QString Lyrics::tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title) { + if (artist.isEmpty() && title.isEmpty()) { + return {}; + } + const QString flat = u"%1/%2 - %3.lrc"_s.arg(dir, sanitizeFilenamePart(artist), sanitizeFilenamePart(title)); + return QFile::exists(flat) ? flat : QString(); +} + +QString Lyrics::findLocalLrcRecursive(const QString& dir, const QString& artist, const QString& title) { + if (dir.isEmpty()) { + return {}; + } + if (artist.isEmpty() && title.isEmpty()) { + return {}; + } + + QDirIterator it(dir, QStringList{ u"*.lrc"_s }, QDir::Files | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + + while (it.hasNext()) { + const QString path = it.next(); + const QString name = it.fileName(); + if ((artist.isEmpty() || containsCi(name, artist)) && (title.isEmpty() || containsCi(name, title))) { + return path; + } + } + return {}; +} + +QVector Lyrics::parseLrc(const QString& text) { + QVector result; + if (text.isEmpty()) { + return result; + } + + static const QRegularExpression timeRegex(u"\\[(\\d+):(\\d+(?:\\.\\d+)?)\\]"_s); + static const QStringList creditKeywords = { + u"作词"_s, + u"作曲"_s, + u"编曲"_s, + u"制作"_s, + u"收录"_s, + u"演奏"_s, + u"词:"_s, + u"曲:"_s, + u"Lyricist"_s, + u"Composer"_s, + u"Arranger"_s, + u"Producer"_s, + u"Mixing"_s, + u"Mastering"_s, + }; + + const QStringList lines = text.split(QLatin1Char('\n')); + for (const QString& line : lines) { + QList matches; + auto it = timeRegex.globalMatch(line); + while (it.hasNext()) { + matches.append(it.next()); + } + if (matches.isEmpty()) { + continue; + } + + QString lyric = line; + lyric.replace(timeRegex, QString()); + lyric = lyric.trimmed(); + + const qreal firstTime = matches.first().captured(1).toInt() * 60.0 + matches.first().captured(2).toDouble(); + + if (firstTime < 20.0) { + bool isCredit = false; + for (const QString& k : creditKeywords) { + if (lyric.contains(k, Qt::CaseInsensitive)) { + isCredit = true; + break; + } + } + if (isCredit && (lyric.contains(QLatin1Char(':')) || lyric.contains(QChar(0xFF1A)) || lyric.size() < 25)) { + continue; + } + } + + for (const auto& m : matches) { + const qreal t = m.captured(1).toInt() * 60.0 + m.captured(2).toDouble(); + result.append(LyricLine{ t, lyric }); + } + } + + std::sort(result.begin(), result.end(), [](const LyricLine& a, const LyricLine& b) { + return a.time < b.time; + }); + + return result; +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/lyrics.hpp b/plugin/src/Caelestia/Services/lyrics.hpp new file mode 100644 index 00000000..e06e8696 --- /dev/null +++ b/plugin/src/Caelestia/Services/lyrics.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include "lyriccandidate.hpp" + +#include +#include +#include +#include + +namespace caelestia::services { + +struct LyricLine { + qreal time = 0.0; + QString text; +}; + +class Lyrics : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QStringList lyrics READ lyrics NOTIFY lyricsChanged) + Q_PROPERTY(caelestia::services::LyricsBackend::Backend backend READ backend NOTIFY backendChanged) + Q_PROPERTY(caelestia::services::LyricsBackend::Backend preferredBackend READ preferredBackend WRITE + setPreferredBackend NOTIFY preferredBackendChanged) + Q_PROPERTY( + QList lyricCandidates READ lyricCandidates NOTIFY lyricCandidatesChanged) + Q_PROPERTY(caelestia::services::LyricCandidate selectedCandidate READ selectedCandidate WRITE setSelectedCandidate + NOTIFY selectedCandidateChanged) + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + Q_PROPERTY(bool hasLyrics READ hasLyrics NOTIFY lyricsChanged) + Q_PROPERTY(qreal offset READ offset WRITE setOffset NOTIFY offsetChanged) + Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackChanged) + Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackChanged) + +public: + explicit Lyrics(QObject* parent = nullptr); + + [[nodiscard]] QStringList lyrics() const; + [[nodiscard]] LyricsBackend::Backend backend() const; + [[nodiscard]] LyricsBackend::Backend preferredBackend() const; + void setPreferredBackend(LyricsBackend::Backend value); + [[nodiscard]] QList lyricCandidates() const; + [[nodiscard]] LyricCandidate selectedCandidate() const; + void setSelectedCandidate(const LyricCandidate& value); + [[nodiscard]] bool loading() const; + [[nodiscard]] bool hasLyrics() const; + [[nodiscard]] qreal offset() const; + void setOffset(qreal value); + [[nodiscard]] QString trackArtist() const; + [[nodiscard]] QString trackTitle() const; + + [[nodiscard]] Q_INVOKABLE int indexForTime(qreal time) const; + Q_INVOKABLE void setTrack( + const QString& artist, const QString& title, const QString& album = {}, qreal duration = 0.0); + Q_INVOKABLE void clearTrack(); + Q_INVOKABLE void refresh(); + +signals: + void lyricsChanged(); + void backendChanged(); + void preferredBackendChanged(); + void lyricCandidatesChanged(); + void selectedCandidateChanged(); + void loadingChanged(); + void hasLyricsChanged(); + void offsetChanged(); + void trackChanged(); + +private: + void setBackend(LyricsBackend::Backend value); + void setLoading(bool value); + void setLines(QVector lines, LyricsBackend::Backend source); + void clearLines(); + void appendCandidates(const QList& add); + void clearCandidates(); + + void scheduleLoad(); + void doLoad(); + void cancelInFlight(); + int newRequestId(); + + void tryLocal(int reqId); + void tryLrclib(int reqId); + void tryNetEase(int reqId); + void chainNext(LyricsBackend::Backend just_failed, int reqId); + + void searchLrclibCandidates(int reqId); + void searchNetEaseCandidates(int reqId); + + void fetchLrclibById(const QString& id, int reqId); + void fetchNetEaseLyricsById(const QString& id, int reqId); + + QNetworkReply* getJson(const QUrl& url, const QHash& headers = {}); + void trackReply(int reqId, QNetworkReply* reply); + + void onPreferredBackendConfigChanged(); + void onLyricsDirChanged(); + + void loadLyricsMap(); + void persistTrackPrefs(); + + [[nodiscard]] QString lyricsDir() const; + [[nodiscard]] QString lyricsMapPath() const; + [[nodiscard]] QString trackKey() const; + [[nodiscard]] static QString backendKey(LyricsBackend::Backend value); + [[nodiscard]] static LyricsBackend::Backend backendFromKey(const QString& key); + + [[nodiscard]] static const QString& cacheDir(); + [[nodiscard]] static QString cachePathFor(LyricsBackend::Backend backend, const QString& id); + [[nodiscard]] static QString readCachedLrc(LyricsBackend::Backend backend, const QString& id); + static void writeCachedLrc(LyricsBackend::Backend backend, const QString& id, const QString& text); + + [[nodiscard]] static QVector parseLrc(const QString& text); + [[nodiscard]] static QString tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title); + [[nodiscard]] static QString findLocalLrcRecursive(const QString& dir, const QString& artist, const QString& title); + + QNetworkAccessManager* m_nam; + QTimer* m_loadDebounce; + + QVector m_lines; + QStringList m_lyrics; + LyricsBackend::Backend m_backend = LyricsBackend::Auto; + LyricsBackend::Backend m_preferredBackend = LyricsBackend::Auto; + QList m_candidates; + LyricCandidate m_selected; + bool m_loading = false; + bool m_hasLyrics = false; + qreal m_offset = 0.0; + + QString m_artist; + QString m_title; + QString m_album; + qreal m_duration = 0.0; + + int m_currentRequestId = 0; + QHash>> m_pendingReplies; + + QJsonObject m_lyricsMap; + bool m_lyricsMapLoaded = false; + bool m_settingFromPrefs = false; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/memory.cpp b/plugin/src/Caelestia/Services/memory.cpp new file mode 100644 index 00000000..e4df3133 --- /dev/null +++ b/plugin/src/Caelestia/Services/memory.cpp @@ -0,0 +1,58 @@ +#include "memory.hpp" + +#include +#include + +namespace caelestia::services { + +Memory::Memory(QObject* parent) + : TickingService(parent) {} + +qreal Memory::used() const { + return m_used; +} + +qreal Memory::total() const { + return m_total; +} + +qreal Memory::percentage() const { + return m_total > 0.0 ? m_used / m_total : 0.0; +} + +void Memory::tick() { + QFile f(QStringLiteral("/proc/meminfo")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + const QByteArray data = f.readAll(); + f.close(); + + static const QRegularExpression reTotal(QStringLiteral("MemTotal: *(\\d+)")); + static const QRegularExpression reAvail(QStringLiteral("MemAvailable: *(\\d+)")); + const QString text = QString::fromLatin1(data); + + const auto totalMatch = reTotal.match(text); + const auto availMatch = reAvail.match(text); + if (!totalMatch.hasMatch() || !availMatch.hasMatch()) { + return; + } + + const quint64 totalKib = totalMatch.captured(1).toULongLong(); + const quint64 availKib = availMatch.captured(1).toULongLong(); + if (totalKib == 0) { + return; + } + const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0; + + if (totalKib == m_lastTotal && usedKib == m_lastUsed) { + return; + } + m_lastTotal = totalKib; + m_lastUsed = usedKib; + m_total = static_cast(totalKib); + m_used = static_cast(usedKib); + Q_EMIT changed(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/memory.hpp b/plugin/src/Caelestia/Services/memory.hpp new file mode 100644 index 00000000..4c8950c2 --- /dev/null +++ b/plugin/src/Caelestia/Services/memory.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "tickingservice.hpp" + +#include +#include + +namespace caelestia::services { + +class Memory : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(qreal used READ used NOTIFY changed) + Q_PROPERTY(qreal total READ total NOTIFY changed) + Q_PROPERTY(qreal percentage READ percentage NOTIFY changed) + +public: + explicit Memory(QObject* parent = nullptr); + + [[nodiscard]] qreal used() const; + [[nodiscard]] qreal total() const; + [[nodiscard]] qreal percentage() const; + +signals: + void changed(); + +protected: + void tick() override; + +private: + qreal m_used = 0.0; + qreal m_total = 1.0; + quint64 m_lastUsed = 0; + quint64 m_lastTotal = 0; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/sensorslib.cpp b/plugin/src/Caelestia/Services/sensorslib.cpp new file mode 100644 index 00000000..7b57731b --- /dev/null +++ b/plugin/src/Caelestia/Services/sensorslib.cpp @@ -0,0 +1,166 @@ +#include "sensorslib.hpp" + +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcSensorsLib, "caelestia.services.sensorslib", QtInfoMsg) + +namespace caelestia::services::sensorslib { + +namespace { + +std::atomic g_initOk{ false }; +std::once_flag g_initFlag; + +void doInit() { + if (sensors_init(nullptr) != 0) { + qCWarning(lcSensorsLib, "sensors_init failed"); + g_initOk.store(false, std::memory_order_release); + return; + } + g_initOk.store(true, std::memory_order_release); + std::atexit([] { + if (g_initOk.load(std::memory_order_acquire)) { + sensors_cleanup(); + } + }); +} + +[[nodiscard]] std::optional readTempInput(const sensors_chip_name* chip, const sensors_feature* feat) { + const sensors_subfeature* sf = sensors_get_subfeature(chip, feat, SENSORS_SUBFEATURE_TEMP_INPUT); + if (!sf) { + return std::nullopt; + } + double value = 0.0; + if (sensors_get_value(chip, sf->number, &value) != 0) { + return std::nullopt; + } + return value; +} + +[[nodiscard]] QByteArray featureLabel(const sensors_chip_name* chip, const sensors_feature* feat) { + char* raw = sensors_get_label(chip, feat); + if (!raw) { + return {}; + } + QByteArray out(raw); + std::free(raw); + return out; +} + +bool labelEquals(const QByteArray& label, const char* literal) { + return label == QByteArrayView(literal); +} + +bool labelStartsWith(const QByteArray& label, const char* prefix) { + const auto n = std::strlen(prefix); + return static_cast(label.size()) >= n && std::memcmp(label.constData(), prefix, n) == 0; +} + +} // namespace + +void ensureInit() { + std::call_once(g_initFlag, doInit); +} + +std::optional cpuPackageTemp() { + ensureInit(); + if (!g_initOk.load(std::memory_order_acquire)) { + return std::nullopt; + } + + std::optional primary; // Package id N / Tdie + std::optional fallback; // Tctl + + int chipNr = 0; + while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) { + int featNr = 0; + while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) { + if (feat->type != SENSORS_FEATURE_TEMP) { + continue; + } + const QByteArray label = featureLabel(chip, feat); + if (label.isEmpty()) { + continue; + } + + if (labelStartsWith(label, "Package id ") || labelEquals(label, "Tdie")) { + if (auto v = readTempInput(chip, feat)) { + primary = v; + } + } else if (labelEquals(label, "Tctl")) { + if (auto v = readTempInput(chip, feat)) { + fallback = v; + } + } + } + } + + return primary.has_value() ? primary : fallback; +} + +std::optional gpuPciAverageTemp() { + ensureInit(); + if (!g_initOk.load(std::memory_order_acquire)) { + return std::nullopt; + } + + double sumPrimary = 0.0; + int countPrimary = 0; + double sumFallback = 0.0; + int countFallback = 0; + + int chipNr = 0; + while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) { + if (chip->bus.type != SENSORS_BUS_TYPE_PCI) { + continue; + } + + int featNr = 0; + while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) { + if (feat->type != SENSORS_FEATURE_TEMP) { + continue; + } + const QByteArray label = featureLabel(chip, feat); + if (label.isEmpty()) { + continue; + } + + const bool tempIndexed = labelStartsWith(label, "temp") && label.size() > 4 && + std::isdigit(static_cast(label[4])); + const bool isPrimary = tempIndexed || labelEquals(label, "GPU core") || labelEquals(label, "edge"); + const bool isFallback = labelEquals(label, "junction") || labelEquals(label, "mem"); + + if (!isPrimary && !isFallback) { + continue; + } + + const auto v = readTempInput(chip, feat); + if (!v) { + continue; + } + if (isPrimary) { + sumPrimary += *v; + ++countPrimary; + } else { + sumFallback += *v; + ++countFallback; + } + } + } + + if (countPrimary > 0) { + return sumPrimary / countPrimary; + } + if (countFallback > 0) { + return sumFallback / countFallback; + } + return std::nullopt; +} + +} // namespace caelestia::services::sensorslib diff --git a/plugin/src/Caelestia/Services/sensorslib.hpp b/plugin/src/Caelestia/Services/sensorslib.hpp new file mode 100644 index 00000000..8c305a6f --- /dev/null +++ b/plugin/src/Caelestia/Services/sensorslib.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace caelestia::services::sensorslib { + +void ensureInit(); + +[[nodiscard]] std::optional cpuPackageTemp(); +[[nodiscard]] std::optional gpuPciAverageTemp(); + +} // namespace caelestia::services::sensorslib diff --git a/plugin/src/Caelestia/Services/storage.cpp b/plugin/src/Caelestia/Services/storage.cpp new file mode 100644 index 00000000..d3e31496 --- /dev/null +++ b/plugin/src/Caelestia/Services/storage.cpp @@ -0,0 +1,312 @@ +#include "storage.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcStorage, "caelestia.services.storage", QtInfoMsg) + +namespace caelestia::services { + +namespace { + +struct Accum { + quint64 usedBytes = 0; + quint64 totalBytes = 0; + bool hasRoot = false; +}; + +[[nodiscard]] QString sysfsRealPath(uint major, uint minor) { + const QString link = QStringLiteral("/sys/dev/block/%1:%2").arg(major).arg(minor); + const QString resolved = QFileInfo(link).canonicalFilePath(); + return resolved; +} + +[[nodiscard]] bool readDevtFromSysfs(const QString& sysfsBlockDir, uint& major, uint& minor) { + QFile f(sysfsBlockDir + QStringLiteral("/dev")); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + const QByteArray line = f.readLine().trimmed(); + f.close(); + + const qsizetype colon = line.indexOf(':'); + if (colon <= 0) { + return false; + } + bool okM = false; + bool okN = false; + major = line.left(colon).toUInt(&okM); + minor = line.mid(colon + 1).toUInt(&okN); + return okM && okN; +} + +QStringList resolveByDevt(uint major, uint minor, int depth = 0); + +QStringList resolveAtNode(const QString& node, int depth) { + if (node.isEmpty() || depth > 8) { + return {}; + } + + const QFileInfo nodeInfo(node); + if (!nodeInfo.exists() || !nodeInfo.isDir()) { + return {}; + } + + if (QFileInfo::exists(node + QStringLiteral("/partition"))) { + const QString diskNode = nodeInfo.path(); + return { QFileInfo(diskNode).fileName() }; + } + + const QDir slavesDir(node + QStringLiteral("/slaves")); + if (slavesDir.exists()) { + const QStringList slaves = slavesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + if (!slaves.isEmpty()) { + QStringList out; + for (const QString& slave : slaves) { + uint sm = 0; + uint sn = 0; + const QString slaveDir = QStringLiteral("/sys/class/block/") + slave; + if (!readDevtFromSysfs(slaveDir, sm, sn)) { + continue; + } + const auto devs = resolveByDevt(sm, sn, depth + 1); + for (const QString& d : devs) { + if (!out.contains(d)) { + out.append(d); + } + } + } + return out; + } + } + + return { nodeInfo.fileName() }; +} + +QStringList resolveByDevt(uint major, uint minor, int depth) { + return resolveAtNode(sysfsRealPath(major, minor), depth); +} + +} // namespace + +Storage::Storage(QObject* parent) + : TickingService(parent) {} + +qreal Storage::percentage() const { + qreal totalUsed = 0.0; + qreal totalSize = 0.0; + for (const DiskInfo* d : m_disks) { + totalUsed += d->used(); + totalSize += d->total(); + } + return totalSize > 0.0 ? totalUsed / totalSize : 0.0; +} + +bool Storage::sameOrder(const QList& a, const QList& b) { + if (a.size() != b.size()) { + return false; + } + for (qsizetype i = 0; i < a.size(); ++i) { + if (a.at(i) != b.at(i)) { + return false; + } + } + return true; +} + +QQmlListProperty Storage::disksProp() { + return QQmlListProperty(this, nullptr, &Storage::disksCount, &Storage::disksAt); +} + +qsizetype Storage::disksCount(QQmlListProperty* prop) { + return static_cast(prop->object)->m_disks.size(); +} + +DiskInfo* Storage::disksAt(QQmlListProperty* prop, qsizetype i) { + return static_cast(prop->object)->m_disks.at(i); +} + +DiskInfo* Storage::manualPrimaryDisk() const { + return m_manualPrimaryDisk.data(); +} + +void Storage::setManualPrimaryDisk(DiskInfo* disk) { + if (m_manualPrimaryDisk.data() == disk) { + return; + } + m_manualPrimaryDisk = disk; + Q_EMIT manualPrimaryDiskChanged(); + Q_EMIT primaryDiskChanged(); +} + +DiskInfo* Storage::primaryDisk() const { + if (auto* m = m_manualPrimaryDisk.data()) { + return m; + } + return m_disks.isEmpty() ? nullptr : m_disks.first(); +} + +bool Storage::isPseudoFs(QByteArrayView fsType) { + static constexpr const char* kPseudo[] = { + "tmpfs", + "devtmpfs", + "proc", + "sysfs", + "cgroup", + "cgroup2", + "overlay", + "squashfs", + "devpts", + "mqueue", + "ramfs", + "rpc_pipefs", + "autofs", + "configfs", + "debugfs", + "tracefs", + "securityfs", + "pstore", + "bpf", + "binfmt_misc", + "hugetlbfs", + "fusectl", + "efivarfs", + "selinuxfs", + }; + for (const char* p : kPseudo) { + if (fsType == QByteArrayView(p)) { + return true; + } + } + return fsType.startsWith(QByteArrayView("fuse.")); +} + +QStringList Storage::resolveToPhysicalDisks(const QString& devicePath) { + if (devicePath.isEmpty() || !devicePath.startsWith(QLatin1Char('/'))) { + return {}; + } + struct stat st{}; + if (::stat(devicePath.toLocal8Bit().constData(), &st) != 0) { + return {}; + } + if (!S_ISBLK(st.st_mode)) { + return {}; + } + return resolveByDevt(major(st.st_rdev), minor(st.st_rdev)); +} + +void Storage::tick() { + const qreal prevPercentage = percentage(); + QHash byDisk; + + // Multiple mounts can share a single backing filesystem (btrfs subvolumes, + // bind mounts, etc.) and each one reports identical bytesTotal/bytesAvailable. + // Dedupe by source device so the filesystem only contributes once per disk. + struct DeviceEntry { + quint64 totalBytes = 0; + quint64 usedBytes = 0; + bool hasRoot = false; + QByteArray device; + }; + + QHash byDevice; + + const auto mountedVols = QStorageInfo::mountedVolumes(); + for (const QStorageInfo& v : mountedVols) { + if (!v.isReady() || !v.isValid() || v.bytesTotal() <= 0) { + continue; + } + if (isPseudoFs(QByteArrayView(v.fileSystemType()))) { + continue; + } + + const QByteArray device = v.device(); + const auto totalBytes = static_cast(v.bytesTotal()); + const auto availBytes = static_cast(v.bytesAvailable()); + const quint64 usedBytes = totalBytes > availBytes ? totalBytes - availBytes : 0; + const bool isRoot = v.rootPath() == QStringLiteral("/"); + + DeviceEntry& e = byDevice[device]; + e.device = device; + e.totalBytes = totalBytes; + e.usedBytes = usedBytes; + e.hasRoot = e.hasRoot || isRoot; + } + + for (auto it = byDevice.constBegin(); it != byDevice.constEnd(); ++it) { + const DeviceEntry& e = it.value(); + const QStringList disks = resolveToPhysicalDisks(QString::fromLocal8Bit(e.device)); + if (disks.isEmpty()) { + continue; + } + for (const QString& d : disks) { + if (d.startsWith(QStringLiteral("zram"))) { + continue; + } + Accum& a = byDisk[d]; + a.usedBytes += e.usedBytes; + a.totalBytes += e.totalBytes; + a.hasRoot = a.hasRoot || e.hasRoot; + } + } + + QHash existing; + existing.reserve(m_disks.size()); + for (DiskInfo* d : std::as_const(m_disks)) { + existing.insert(d->mount(), d); + } + + QList next; + next.reserve(byDisk.size()); + for (auto it = byDisk.constBegin(); it != byDisk.constEnd(); ++it) { + if (DiskInfo* survivor = existing.take(it.key())) { + survivor->update(it.value().usedBytes, it.value().totalBytes, it.value().hasRoot); + next.append(survivor); + } else { + next.append(new DiskInfo(it.key(), it.value().usedBytes, it.value().totalBytes, it.value().hasRoot, this)); + } + } + + std::sort(next.begin(), next.end(), [](const DiskInfo* a, const DiskInfo* b) { + if (a->hasRoot() != b->hasRoot()) { + return a->hasRoot(); + } + return a->mount() < b->mount(); + }); + + bool manualCleared = false; + if (DiskInfo* m = m_manualPrimaryDisk.data(); m && existing.contains(m->mount())) { + m_manualPrimaryDisk.clear(); + manualCleared = true; + } + for (DiskInfo* stale : std::as_const(existing)) { + stale->deleteLater(); + } + + const bool listChanged = !sameOrder(m_disks, next); + DiskInfo* prevPrimary = primaryDisk(); + m_disks = next; + + if (listChanged) { + Q_EMIT disksChanged(); + } + if (std::abs(percentage() - prevPercentage) > 0.0001) { + Q_EMIT percentageChanged(); + } + if (manualCleared) { + Q_EMIT manualPrimaryDiskChanged(); + } + if (primaryDisk() != prevPrimary) { + Q_EMIT primaryDiskChanged(); + } +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/storage.hpp b/plugin/src/Caelestia/Services/storage.hpp new file mode 100644 index 00000000..a3ee6beb --- /dev/null +++ b/plugin/src/Caelestia/Services/storage.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "diskinfo.hpp" +#include "tickingservice.hpp" + +#include +#include +#include +#include + +namespace caelestia::services { + +class Storage : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) + Q_PROPERTY(QQmlListProperty disks READ disksProp NOTIFY disksChanged) + Q_PROPERTY(caelestia::services::DiskInfo* manualPrimaryDisk READ manualPrimaryDisk WRITE setManualPrimaryDisk NOTIFY + manualPrimaryDiskChanged) + Q_PROPERTY(caelestia::services::DiskInfo* primaryDisk READ primaryDisk NOTIFY primaryDiskChanged) + +public: + explicit Storage(QObject* parent = nullptr); + + [[nodiscard]] qreal percentage() const; + [[nodiscard]] QQmlListProperty disksProp(); + [[nodiscard]] DiskInfo* manualPrimaryDisk() const; + void setManualPrimaryDisk(DiskInfo* disk); + [[nodiscard]] DiskInfo* primaryDisk() const; + +signals: + void disksChanged(); + void percentageChanged(); + void manualPrimaryDiskChanged(); + void primaryDiskChanged(); + +protected: + void tick() override; + +private: + [[nodiscard]] static QStringList resolveToPhysicalDisks(const QString& devicePath); + [[nodiscard]] static bool isPseudoFs(QByteArrayView fsType); + [[nodiscard]] static bool sameOrder(const QList& a, const QList& b); + + static qsizetype disksCount(QQmlListProperty* prop); + static DiskInfo* disksAt(QQmlListProperty* prop, qsizetype i); + + QList m_disks; + QPointer m_manualPrimaryDisk; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/tickingservice.cpp b/plugin/src/Caelestia/Services/tickingservice.cpp new file mode 100644 index 00000000..78454f5e --- /dev/null +++ b/plugin/src/Caelestia/Services/tickingservice.cpp @@ -0,0 +1,51 @@ +#include "tickingservice.hpp" + +#include "../Config/config.hpp" +#include "../Config/dashboardconfig.hpp" + +namespace caelestia::services { + +TickingService::TickingService(QObject* parent) + : Service(parent) + , m_timer(new QTimer(this)) { + m_timer->setSingleShot(false); + QObject::connect(m_timer, &QTimer::timeout, this, [this] { + tick(); + }); + + auto* dash = caelestia::config::GlobalConfig::instance()->dashboard(); + applyInterval(dash->resourceUpdateInterval()); + QObject::connect(dash, &caelestia::config::DashboardConfig::resourceUpdateIntervalChanged, this, [this, dash] { + applyInterval(dash->resourceUpdateInterval()); + }); +} + +int TickingService::updateInterval() const { + return m_interval; +} + +void TickingService::start() { + m_running = true; + if (m_interval > 0) { + m_timer->start(m_interval); + } + tick(); +} + +void TickingService::stop() { + m_running = false; + m_timer->stop(); +} + +void TickingService::applyInterval(int ms) { + if (ms <= 0 || ms == m_interval) { + return; + } + m_interval = ms; + if (m_running) { + m_timer->start(m_interval); + } + Q_EMIT updateIntervalChanged(); +} + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/tickingservice.hpp b/plugin/src/Caelestia/Services/tickingservice.hpp new file mode 100644 index 00000000..721b35d0 --- /dev/null +++ b/plugin/src/Caelestia/Services/tickingservice.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "service.hpp" + +namespace caelestia::services { + +class TickingService : public Service { + Q_OBJECT + + Q_PROPERTY(int updateInterval READ updateInterval NOTIFY updateIntervalChanged) + +public: + explicit TickingService(QObject* parent = nullptr); + + [[nodiscard]] int updateInterval() const; + +signals: + void updateIntervalChanged(); + +protected: + void start() final; + void stop() final; + + virtual void tick() = 0; + +private: + void applyInterval(int ms); + + QTimer* m_timer; + int m_interval = 1000; + bool m_running = false; +}; + +} // namespace caelestia::services diff --git a/plugin/src/Caelestia/Services/usagefmt.cpp b/plugin/src/Caelestia/Services/usagefmt.cpp new file mode 100644 index 00000000..75ffe626 --- /dev/null +++ b/plugin/src/Caelestia/Services/usagefmt.cpp @@ -0,0 +1,33 @@ +#include "usagefmt.hpp" + +namespace { + +constexpr qreal kKib = 1024.0; +constexpr qreal kMib = kKib * 1024.0; +constexpr qreal kGib = kMib * 1024.0; + +bool finitePositive(qreal v) { + return std::isfinite(v) && v >= 0.0; +} + +} // namespace + +namespace caelestia::services::usagefmt { + +FormatResult UsageFmt::formatKib(qreal kib, qreal total) const { + if (!finitePositive(kib) || !finitePositive(total)) { + return { 0.0, 0.0, "KiB" }; + } + if (total >= kGib) { + return { kib / kGib, total / kGib, "TiB" }; + } + if (total >= kMib) { + return { kib / kMib, total / kMib, "GiB" }; + } + if (total >= kKib) { + return { kib / kKib, total / kKib, "MiB" }; + } + return { kib, total, "KiB" }; +} + +} // namespace caelestia::services::usagefmt diff --git a/plugin/src/Caelestia/Services/usagefmt.hpp b/plugin/src/Caelestia/Services/usagefmt.hpp new file mode 100644 index 00000000..2eee3c1c --- /dev/null +++ b/plugin/src/Caelestia/Services/usagefmt.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace caelestia::services::usagefmt { + +struct FormatResult { + Q_GADGET + QML_ANONYMOUS + + Q_PROPERTY(qreal value MEMBER value CONSTANT) + Q_PROPERTY(qreal total MEMBER total CONSTANT) + Q_PROPERTY(QString unit MEMBER unit CONSTANT) + +public: + qreal value; + qreal total; + QString unit; +}; + +class UsageFmt : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib, qreal total) const; +}; + +} // namespace caelestia::services::usagefmt diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index 28a0e178..9c2426c2 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -100,7 +100,7 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q }); } -bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { +bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) { if (!source.isLocalFile()) { qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file"; return false; @@ -120,7 +120,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co return QFile::copy(source.toLocalFile(), target.toLocalFile()); } -bool CUtils::deleteFile(const QUrl& path) const { +bool CUtils::deleteFile(const QUrl& path) { if (!path.isLocalFile()) { qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file"; return false; @@ -129,7 +129,7 @@ bool CUtils::deleteFile(const QUrl& path) const { return QFile::remove(path.toLocalFile()); } -QString CUtils::toLocalFile(const QUrl& url) const { +QString CUtils::toLocalFile(const QUrl& url) { if (!url.isLocalFile()) { qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url; return QString(); @@ -138,4 +138,8 @@ QString CUtils::toLocalFile(const QUrl& url) const { return url.toLocalFile(); } +qreal CUtils::clamp(qreal value, qreal min, qreal max) { + return qBound(min, value, max); +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp index 027226d6..29ae3ca5 100644 --- a/plugin/src/Caelestia/cutils.hpp +++ b/plugin/src/Caelestia/cutils.hpp @@ -21,9 +21,11 @@ public: Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); // clang-format on - Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; - Q_INVOKABLE bool deleteFile(const QUrl& path) const; - Q_INVOKABLE QString toLocalFile(const QUrl& url) const; + 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 caelestia diff --git a/services/Colours.qml b/services/Colours.qml index 922b51db..301f2e7f 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -25,6 +25,9 @@ Singleton { readonly property Transparency transparency: Transparency {} readonly property alias wallLuminance: analyser.luminance + property bool cooldownPending + property real lastBaseTransparency + function getLuminance(c: color): real { if (c.r == 0 && c.g == 0 && c.b == 0) return 0; @@ -84,7 +87,16 @@ Singleton { Hypr.extras.batchMessage([str.arg("blur").arg(transparency.enabled ? 1 : 0), str.arg("ignore_alpha").arg(transparency.base - 0.03)]); } - Component.onCompleted: debounceTimer.triggered() + function requestReloadHyprRules(): void { + if (cooldownTimer.running) { + root.cooldownPending = true; + } else { + root.reloadHyprRules(); + cooldownTimer.restart(); + } + } + + Component.onCompleted: root.requestReloadHyprRules() Connections { function onConfigReloaded(): void { @@ -108,10 +120,23 @@ Singleton { } Timer { - id: debounceTimer + id: cooldownTimer - interval: 300 - onTriggered: root.reloadHyprRules() + interval: 30 + onTriggered: { + if (root.cooldownPending) { + root.cooldownPending = false; + root.reloadHyprRules(); + restart(); + } + } + } + + Timer { + id: cAnimCompleteTimer + + interval: Tokens.anim.durations.expressiveSlowEffects + onTriggered: root.requestReloadHyprRules() } component Transparency: QtObject { @@ -119,8 +144,19 @@ Singleton { readonly property real base: Math.max(0, Math.min(1, Tokens.transparency.base - (root.light ? 0.1 : 0))) readonly property real layers: Tokens.transparency.layers - onEnabledChanged: debounceTimer.restart() - onBaseChanged: debounceTimer.restart() + onEnabledChanged: { + if (enabled) + root.requestReloadHyprRules(); + else + cAnimCompleteTimer.start(); + } + onBaseChanged: { + if (root.lastBaseTransparency > base) + root.requestReloadHyprRules(); + else + cAnimCompleteTimer.start(); + root.lastBaseTransparency = base; + } } component M3TPalette: QtObject { diff --git a/services/LyricsService.qml b/services/LyricsService.qml deleted file mode 100644 index 26a75e9b..00000000 --- a/services/LyricsService.qml +++ /dev/null @@ -1,403 +0,0 @@ -pragma Singleton - -import "../utils/scripts/lrcparser.js" as Lrc -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia -import Caelestia.Config -import qs.utils - -Singleton { - id: root - - property var player: Players.active - property int currentIndex: -1 - property bool loading: false - property bool isManualSeeking: false - property bool lyricsVisible: GlobalConfig.services.showLyrics - property string backend: "Local" - property string preferredBackend: GlobalConfig.services.lyricsBackend - property real currentSongId: 0 - property string loadedLocalFile: "" - property real offset - property int currentRequestId: 0 - property var lyricsMap: ({}) - - readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) - readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" - readonly property alias model: lyricsModel - readonly property alias candidatesModel: fetchedCandidatesModel - readonly property var _netEaseHeaders: ({ - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", - "Referer": "https://music.163.com/" - }) - - function getMetadata() { - if (!player || !player.metadata) - return null; - let artist = player.metadata["xesam:artist"]; - const title = player.metadata["xesam:title"]; - if (Array.isArray(artist)) - artist = artist.join(", "); - return { - artist: artist || "Unknown", - title: title || "Unknown" - }; - } - - function _metaKey(meta) { - return `${meta.artist} - ${meta.title}`; - } - - function savePrefs() { - let meta = getMetadata(); - if (!meta) - return; - let key = _metaKey(meta); - let existing = root.lyricsMap[key] ?? {}; - root.lyricsMap[key] = { - offset: root.offset, - backend: root.backend, - neteaseId: existing.neteaseId ?? null - }; - // reassign to notify QML bindings of the map change - root.lyricsMap = root.lyricsMap; - saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; - saveLyricsMap.running = true; - } - - function toggleVisibility() { - GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; - } - - function loadLyrics() { - loadDebounce.restart(); - } - - function _doLoadLyrics() { - const meta = getMetadata(); - if (!meta) { - lyricsModel.clear(); - root.currentIndex = -1; - return; - } - - loading = true; - lyricsModel.clear(); - currentIndex = -1; - root.currentSongId = 0; - - root.currentRequestId++; - let requestId = root.currentRequestId; - - let key = _metaKey(meta); - let saved = root.lyricsMap[key]; - root.offset = saved?.offset ?? 0.0; - - if (root.preferredBackend === "NetEase") { - root.backend = "NetEase"; - fetchNetEase(meta.title, meta.artist, requestId); - return; - } - - if (root.preferredBackend === "Local") { - root.backend = "Local"; - let cleanDir = lyricsDir.replace(/\/$/, ""); - let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - - // Search for files matching "Artist - Title.lrc" pattern - const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); - const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); - const escapedTitle = titleStr.replace(/'/g, "'\\''"); - const escapedArtist = artistStr.replace(/'/g, "'\\''"); - findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; - findLyricsInSubdirs.requestId = requestId; - findLyricsInSubdirs.running = true; - - lrcFile.path = ""; - lrcFile.path = flatPath; - return; - } - - // Auto mode: try local first - root.backend = "Local"; - let cleanDir = lyricsDir.replace(/\/$/, ""); - let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - - const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); - const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); - const escapedTitle = titleStr.replace(/'/g, "'\\''"); - const escapedArtist = artistStr.replace(/'/g, "'\\''"); - findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; - findLyricsInSubdirs.requestId = requestId; - findLyricsInSubdirs.running = true; - - lrcFile.path = ""; - lrcFile.path = flatPath; - fetchNetEaseCandidates(meta.title, meta.artist, requestId); - } - - function updateModel(parsedArray) { - root.currentIndex = -1; - lyricsModel.clear(); - for (let line of parsedArray) { - lyricsModel.append({ - time: line.time, - lyricLine: line.text - }); - } - } - - function fallbackToOnline() { - let meta = getMetadata(); - if (!meta) - return; - fetchNetEase(meta.title, meta.artist, root.currentRequestId); - } - - // NetEase - - // searches NetEase and populates the candidates model. returns the result array via the onResults callback - function _searchNetEase(title, artist, reqId, onResults) { - Requests.resetCookies(); - const query = encodeURIComponent(`${title} ${artist}`); - const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; - - Requests.get(url, text => { - if (reqId !== root.currentRequestId) - return; - const res = JSON.parse(text); - const songs = res.result?.songs || []; - - fetchedCandidatesModel.clear(); - for (let s of songs) { - fetchedCandidatesModel.append({ - id: s.id, - title: s.name || "Unknown Title", - artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" - }); - } - - onResults(songs); - }, err => {}, root._netEaseHeaders); - } - - // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. - function fetchNetEaseCandidates(title, artist, reqId) { - _searchNetEase(title, artist, reqId, _songs => {}); - } - - // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. - function fetchNetEase(title, artist, reqId) { - _searchNetEase(title, artist, reqId, songs => { - const bestMatch = songs.find(s => { - const inputArtist = String(artist || "").toLowerCase(); - const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); - return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); - }); - - if (!bestMatch) { - return; // No reliable lyrics found - } - - let key = `${artist} - ${title}`; - root.lyricsMap[key] = { - offset: root.lyricsMap[key]?.offset ?? 0.0, - backend: "NetEase", - neteaseId: bestMatch.id - }; - root.currentSongId = bestMatch.id; - savePrefs(); - fetchNetEaseLyrics(bestMatch.id, reqId); - }); - } - - function fetchNetEaseLyrics(id, reqId) { - const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; - Requests.get(url, text => { - if (reqId !== root.currentRequestId) - return; - const res = JSON.parse(text); - if (res.lrc?.lyric) { - updateModel(Lrc.parseLrc(res.lrc.lyric)); - loading = false; - } - }); - } - - function selectCandidate(songId) { - let meta = getMetadata(); - if (!meta) - return; - root.backend = "NetEase"; - root.currentSongId = songId; - let key = _metaKey(meta); - root.lyricsMap[key] = { - offset: root.lyricsMap[key]?.offset ?? 0.0, - neteaseId: songId - }; - savePrefs(); - fetchNetEaseLyrics(songId, currentRequestId); - } - - function updatePosition() { - if (isManualSeeking || loading || !player || lyricsModel.count === 0) - return; - - let pos = player.position - root.offset; - let newIdx = -1; - for (let i = lyricsModel.count - 1; i >= 0; i--) { - if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor - newIdx = i; - break; - } - } - - if (newIdx !== currentIndex) { - root.currentIndex = newIdx; - } - } - - function jumpTo(index, time) { - root.isManualSeeking = true; - root.currentIndex = index; - - if (player) { - player.position = time + root.offset + 0.01; // compensate for rounding - } - - seekTimer.restart(); - } - - onPreferredBackendChanged: { - if (GlobalConfig.services.lyricsBackend !== preferredBackend) { - GlobalConfig.services.lyricsBackend = preferredBackend; - } - } - - ListModel { - id: lyricsModel - } - - ListModel { - id: fetchedCandidatesModel - } - - Timer { - id: seekTimer - - interval: 500 - onTriggered: root.isManualSeeking = false - } - - // If no local lyrics were loaded within the interval, fall back to NetEase - Timer { - id: fallbackTimer - - interval: 200 - onTriggered: { - if (lyricsModel.count === 0) { - root.backend = "NetEase"; - fallbackToOnline(); - } - } - } - - Timer { - id: loadDebounce - - interval: 50 - onTriggered: root._doLoadLyrics() - } - - FileView { - id: lyricsMapFileView - - path: root.lyricsMapFile - printErrors: false - onLoaded: { - try { - root.lyricsMap = JSON.parse(text()); - } catch (e) { - root.lyricsMap = {}; - } - } - } - - FileView { - id: lrcFile - - printErrors: false - onLoaded: { - fallbackTimer.stop(); - let parsed = Lrc.parseLrc(text()); - if (parsed.length > 0) { - root.backend = "Local"; - root.loadedLocalFile = path; - updateModel(parsed); - loading = false; - } else if (root.preferredBackend === "Local") { - // Local mode only - fail immediately - root.backend = "NetEase"; - fallbackToOnline(); - } - // In Auto mode, let the Process onExited handle fallback - } - } - - Connections { - function onActiveChanged() { - root.player = Players.active; - loadLyrics(); - } - - target: Players - } - - Connections { - function onMetadataChanged() { - loadLyrics(); - } - - target: root.player - ignoreUnknownSignals: true - } - - Process { - id: saveLyricsMap - - command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] - } - - Process { - id: findLyricsInSubdirs - - property int requestId: -1 - property bool foundFile: false - - stdout: SplitParser { - onRead: data => { - if (findLyricsInSubdirs.requestId === root.currentRequestId) { - const foundPath = data.trim(); - if (foundPath && foundPath.length > 0) { - findLyricsInSubdirs.foundFile = true; - fallbackTimer.stop(); - root.loadedLocalFile = foundPath; - lrcFile.path = ""; - lrcFile.path = foundPath; - } - } - } - } - - onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters - if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { - if (lyricsModel.count === 0) { - fallbackTimer.restart(); - } - } - foundFile = false; - } - } -} diff --git a/services/Players.qml b/services/Players.qml index 41507839..59f96214 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -16,6 +16,8 @@ Singleton { property alias manualActive: props.manualActive function getIdentity(player: MprisPlayer): string { + if (!player) + return ""; const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity); return alias?.to ?? player.identity; } diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml deleted file mode 100644 index 15dda611..00000000 --- a/services/SystemUsage.qml +++ /dev/null @@ -1,330 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia.Config - -Singleton { - id: root - - // CPU properties - property string cpuName: "" - property real cpuPerc - property real cpuTemp - - // GPU properties - readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType - property string autoGpuType: "NONE" - property string gpuName: "" - property real gpuPerc - property real gpuTemp - - // Memory properties - property real memUsed - property real memTotal - readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 - - // Storage properties (aggregated) - readonly property real storagePerc: { - let totalUsed = 0; - let totalSize = 0; - for (const disk of disks) { - totalUsed += disk.used; - totalSize += disk.total; - } - return totalSize > 0 ? totalUsed / totalSize : 0; - } - - // Individual disks: Array of { mount, used, total, free, perc } - property var disks: [] - - property real lastCpuIdle - property real lastCpuTotal - - property int refCount - - function cleanCpuName(name: string): string { - return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); - } - - function cleanGpuName(name: string): string { - return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); - } - - function formatKib(kib: real): var { - const mib = 1024; - const gib = 1024 ** 2; - const tib = 1024 ** 3; - - if (kib >= tib) - return { - value: kib / tib, - unit: "TiB" - }; - if (kib >= gib) - return { - value: kib / gib, - unit: "GiB" - }; - if (kib >= mib) - return { - value: kib / mib, - unit: "MiB" - }; - return { - value: kib, - unit: "KiB" - }; - } - - Timer { - running: root.refCount > 0 - interval: GlobalConfig.dashboard.resourceUpdateInterval - repeat: true - triggeredOnStart: true - onTriggered: { - stat.reload(); - meminfo.reload(); - storage.running = true; - gpuUsage.running = true; - sensors.running = true; - } - } - - // One-time CPU info detection (name) - FileView { - id: cpuinfoInit - - path: "/proc/cpuinfo" - onLoaded: { - const nameMatch = text().match(/model name\s*:\s*(.+)/); - if (nameMatch) - root.cpuName = root.cleanCpuName(nameMatch[1]); - } - } - - FileView { - id: stat - - path: "/proc/stat" - onLoaded: { - const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); - if (data) { - const stats = data.slice(1).map(n => parseInt(n, 10)); - const total = stats.reduce((a, b) => a + b, 0); - const idle = stats[3] + (stats[4] ?? 0); - - const totalDiff = total - root.lastCpuTotal; - const idleDiff = idle - root.lastCpuIdle; - root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; - - root.lastCpuTotal = total; - root.lastCpuIdle = idle; - } - } - } - - FileView { - id: meminfo - - path: "/proc/meminfo" - onLoaded: { - const data = text(); - root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; - root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; - } - } - - Process { - id: storage - - // Get physical disks with aggregated usage from their partitions - // -J triggers JSON output. -b triggers bytes. - command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] - - stdout: StdioCollector { - onStreamFinished: { - const data = JSON.parse(text); - const diskList = []; - const seenDevices = new Set(); - - // Helper to recursively sum usage from children (partitions, crypt, lvm) - const aggregateUsage = dev => { - let used = 0; - let size = 0; - let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); - - if (!seenDevices.has(dev.name)) { - // lsblk returns null for empty/unformatted partitions, which parses to 0 here - used = parseInt(dev.fsused) || 0; - size = parseInt(dev.fssize) || 0; - seenDevices.add(dev.name); - } - - if (dev.children) { - for (const child of dev.children) { - const stats = aggregateUsage(child); - used += stats.used; - size += stats.size; - if (stats.isRoot) - isRoot = true; - } - } - return { - used, - size, - isRoot - }; - }; - - for (const dev of data.blockdevices) { - // Only process physical disks at the top level - if (dev.type === "disk" && !dev.name.startsWith("zram")) { - const stats = aggregateUsage(dev); - - if (stats.size === 0) { - continue; - } - - const total = stats.size; - const used = stats.used; - - diskList.push({ - mount: dev.name, - used: used / 1024 // KiB - , - total: total / 1024 // KiB - , - free: (total - used) / 1024, - perc: total > 0 ? used / total : 0, - hasRoot: stats.isRoot - }); - } - } - - // Sort by putting the disk with root first, then sort the rest alphabetically - root.disks = diskList.sort((a, b) => { - if (a.hasRoot && !b.hasRoot) - return -1; - if (!a.hasRoot && b.hasRoot) - return 1; - return a.mount.localeCompare(b.mount); - }); - } - } - } - - // GPU name detection (one-time) - Process { - id: gpuNameDetect - - running: true - command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"] - stdout: StdioCollector { - onStreamFinished: { - const output = text.trim(); - if (!output) - return; - - // Check if it's from nvidia-smi (clean GPU name) - if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { - root.gpuName = root.cleanGpuName(output); - } else if (output.toLowerCase().includes("rx")) { - root.gpuName = root.cleanGpuName(output); - } else { - // Parse lspci output: extract name from brackets or after colon - // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) - const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); - if (bracketMatch) { - root.gpuName = root.cleanGpuName(bracketMatch[1]); - } else { - const colonMatch = output.match(/:\s*(.+)/); - if (colonMatch) - root.gpuName = root.cleanGpuName(colonMatch[1]); - } - } - } - } - } - - Process { - id: gpuTypeCheck - - running: !GlobalConfig.services.gpuType - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() - } - } - - Process { - id: gpuUsage - - command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"] - stdout: StdioCollector { - onStreamFinished: { - if (root.gpuType === "GENERIC") { - const percs = text.trim().split("\n"); - const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); - root.gpuPerc = sum / percs.length / 100; - } else if (root.gpuType === "NVIDIA") { - const [usage, temp] = text.trim().split(","); - root.gpuPerc = parseInt(usage, 10) / 100; - root.gpuTemp = parseInt(temp, 10); - } else { - root.gpuPerc = 0; - root.gpuTemp = 0; - } - } - } - } - - Process { - id: sensors - - command: ["sensors"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/); - if (!cpuTemp) - // If AMD Tdie pattern failed, try fallback on Tctl - cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); - - if (cpuTemp) - root.cpuTemp = parseFloat(cpuTemp[1]); - - if (root.gpuType !== "GENERIC") - return; - - let eligible = false; - let sum = 0; - let count = 0; - - for (const line of text.trim().split("\n")) { - if (line === "Adapter: PCI adapter") - eligible = true; - else if (line === "") - eligible = false; - else if (eligible) { - let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); - if (!match) - // Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs) - match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); - - if (match) { - sum += parseFloat(match[2]); - count++; - } - } - } - - root.gpuTemp = count > 0 ? sum / count : 0; - } - } - } -} diff --git a/services/Weather.qml b/services/Weather.qml index 9c30bf7a..f2b36eec 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -17,8 +17,8 @@ Singleton { readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string description: cc?.weatherDesc ?? qsTr("No weather") - readonly property string temp: GlobalConfig.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` - readonly property string feelsLike: GlobalConfig.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` + readonly property string temp: formatTemp(cc?.tempC) + readonly property string feelsLike: formatTemp(cc?.feelsLikeC) readonly property int humidity: cc?.humidity ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0 readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" @@ -26,6 +26,10 @@ Singleton { readonly property var cachedCities: new Map() + function formatTemp(temp: var): string { + return GlobalConfig.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`; + } + function reload(): void { const configLocation = GlobalConfig.services.weatherLocation; @@ -114,10 +118,8 @@ Singleton { cc = { weatherCode: json.current.weather_code, weatherDesc: getWeatherCondition(json.current.weather_code), - tempC: Math.round(json.current.temperature_2m), - tempF: Math.round(toFahrenheit(json.current.temperature_2m)), - feelsLikeC: Math.round(json.current.apparent_temperature), - feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)), + tempC: json.current.temperature_2m, + feelsLikeC: json.current.apparent_temperature, humidity: json.current.relative_humidity_2m, windSpeed: json.current.wind_speed_10m, isDay: json.current.is_day, @@ -129,10 +131,8 @@ Singleton { for (let i = 0; i < json.daily.time.length; i++) forecastList.push({ date: json.daily.time[i].replace(/-/g, "/"), - maxTempC: Math.round(json.daily.temperature_2m_max[i]), - maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), - minTempC: Math.round(json.daily.temperature_2m_min[i]), - minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])), + maxTempC: json.daily.temperature_2m_max[i], + minTempC: json.daily.temperature_2m_min[i], weatherCode: json.daily.weather_code[i], icon: Icons.getWeatherIcon(json.daily.weather_code[i]) }); @@ -150,7 +150,7 @@ Singleton { timestamp: json.hourly.time[i], hour: time.getHours(), tempC: Math.round(json.hourly.temperature_2m[i]), - tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])), + precipChance: json.hourly.precipitation_probability[i], weatherCode: json.hourly.weather_code[i], icon: Icons.getWeatherIcon(json.hourly.weather_code[i]) }); @@ -169,7 +169,7 @@ Singleton { const [lat, lon] = loc.split(",").map(s => s.trim()); const baseUrl = "https://api.open-meteo.com/v1/forecast"; - const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; + const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; return baseUrl + "?" + params.join("&"); } diff --git a/shell.qml b/shell.qml index bc2bb164..273d541c 100644 --- a/shell.qml +++ b/shell.qml @@ -14,6 +14,8 @@ import Quickshell ShellRoot { settings.watchFiles: true + GSFLoader {} + Background {} Drawers {} AreaPicker {} diff --git a/utils/Icons.qml b/utils/Icons.qml index c864d553..5bbbff57 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -241,23 +241,12 @@ Singleton { return icon; } - function getBatteryIcon(charge: int): string { - if (charge > 0 && charge < 5) - return "battery_0_bar"; - if (charge >= 5 && charge < 20) - return "battery_1_bar"; - if (charge >= 20 && charge < 35) - return "battery_2_bar"; - if (charge >= 35 && charge < 50) - return "battery_3_bar"; - if (charge >= 50 && charge < 65) - return "battery_4_bar"; - if (charge >= 65 && charge < 80) - return "battery_5_bar"; - if (charge >= 80 && charge < 95) - return "battery_6_bar"; - if (charge >= 95) - return "battery_full"; - return "battery_alert"; + function getBatteryIcon(percentage: real, charging = false): string { + if (percentage === 1) + return charging ? "battery_charging_full" : "battery_full"; + let level = Math.floor(percentage * 7); + if (charging && (level === 4 || level === 1)) + level--; + return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } } diff --git a/utils/Searcher.qml b/utils/Searcher.qml index 102c9e76..4a1c1f85 100644 --- a/utils/Searcher.qml +++ b/utils/Searcher.qml @@ -35,7 +35,7 @@ Singleton { } function query(search: string): list { - search = transformSearch(search); + search = transformSearch(search.trim().replace(/\s+/g, " ")); if (!search) return [...list]; diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js deleted file mode 100644 index 847779ed..00000000 --- a/utils/scripts/lrcparser.js +++ /dev/null @@ -1,62 +0,0 @@ -function parseLrc(text) { - if (!text) return []; - let lines = text.split("\n"); - let result = []; - - let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; - - // Blacklist for credits/metadata often found in NetEase lyrics - const creditKeywords = [ - "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" - ]; - - for (let line of lines) { - - timeRegex.lastIndex = 0; - let matches = []; - let match; - - while ((match = timeRegex.exec(line)) !== null) { - matches.push(match); - } - - if (matches.length === 0) continue; - - let lyric = line.replace(timeRegex, "").trim(); - - let min = parseInt(matches[0][1]); - let sec = parseFloat(matches[0][2]); - let totalTime = min * 60 + sec; - - // Only filter credits if they appear in the first 20 seconds - if (totalTime < 20) { - let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); - if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { - continue; - } - } - - for (let match of matches) { - let min = parseInt(match[1]); - let sec = parseFloat(match[2]); - - result.push({ - time: min * 60 + sec, - text: lyric - }); - } - } - - result.sort((a, b) => a.time - b.time); - return result; -} - -function getCurrentLine(lyrics, position) { - const epsilon = 0.1; // 100ms tolerance - for (let i = lyrics.length - 1; i >= 0; i--) { - if ((position + epsilon) >= lyrics[i].time) { - return i; - } - } - return -1; -}