From ea82b8cf228a25f47837713360a27ba16a113e0e Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 16 Jun 2026 10:55:30 +0200 Subject: [PATCH] calendar month change animation --- Modules/Dashboard/Content.qml | 4 +- Modules/Dashboard/Dash.qml | 6 +- Modules/Dashboard/Dash/Calendar.qml | 433 ++++++++++++++++++---------- Modules/Dashboard/Dash/User.qml | 2 +- Modules/Dashboard/Wrapper.qml | 2 +- 5 files changed, 295 insertions(+), 152 deletions(-) diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index 867004e..4678257 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -10,9 +10,9 @@ import qs.Components Item { id: root + required property PersistentProperties dashState readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 - required property PersistentProperties state required property PersistentProperties visibilities implicitHeight: nonAnimHeight @@ -59,7 +59,7 @@ Item { index: 0 sourceComponent: Dash { - state: root.state + dashState: root.dashState visibilities: root.visibilities } } diff --git a/Modules/Dashboard/Dash.qml b/Modules/Dashboard/Dash.qml index d7a83f8..56066cb 100644 --- a/Modules/Dashboard/Dash.qml +++ b/Modules/Dashboard/Dash.qml @@ -10,9 +10,9 @@ import qs.Modules.Dashboard.Dash GridLayout { id: root + required property PersistentProperties dashState readonly property bool dashboardVisible: visibilities.dashboard property int radius: Appearance.rounding.smallest - required property PersistentProperties state required property PersistentProperties visibilities columnSpacing: Appearance.spacing.smaller @@ -60,7 +60,7 @@ GridLayout { User { id: user - state: root.state + dashState: root.dashState } } @@ -86,7 +86,7 @@ GridLayout { Calendar { id: calendar - state: root.state + dashState: root.dashState } } diff --git a/Modules/Dashboard/Dash/Calendar.qml b/Modules/Dashboard/Dash/Calendar.qml index be9055c..d9a78b5 100644 --- a/Modules/Dashboard/Dash/Calendar.qml +++ b/Modules/Dashboard/Dash/Calendar.qml @@ -1,25 +1,50 @@ pragma ComponentBehavior: Bound +import Quickshell import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Components -import qs.Helpers import qs.Config -import qs.Modules CustomMouseArea { id: root - readonly property int currMonth: state.currentDate.getMonth() - readonly property int currYear: state.currentDate.getFullYear() - required property var state + property int activeGrid: 1 + required property PersistentProperties dashState + property bool initialized: false + property int month1 + property int month2 + readonly property int realCurrMonth: dashState.currentDate.getMonth() + readonly property int realCurrYear: dashState.currentDate.getFullYear() + property int year1 + property int year2 + + function handleDateChange() { + const currM = activeGrid === 1 ? month1 : month2; + const currY = activeGrid === 1 ? year1 : year2; + if (realCurrMonth !== currM || realCurrYear !== currY) { + monthChangeAnim.direction = (realCurrYear > currY || (realCurrYear === currY && realCurrMonth > currM)) ? -1 : 1; + + if (activeGrid === 1) { + month2 = realCurrMonth; + year2 = realCurrYear; + activeGrid = 2; + } else { + month1 = realCurrMonth; + year1 = realCurrYear; + activeGrid = 1; + } + if (initialized) + monthChangeAnim.restart(); + } + } function onWheel(event: WheelEvent): void { if (event.angleDelta.y > 0) - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1); else if (event.angleDelta.y < 0) - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1); } acceptedButtons: Qt.MiddleButton @@ -27,102 +52,160 @@ CustomMouseArea { anchors.right: parent.right implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 - onClicked: root.state.currentDate = new Date() + Component.onCompleted: { + month1 = realCurrMonth; + year1 = realCurrYear; + month2 = realCurrMonth; + year2 = realCurrYear; + + initialized = true; + } + onClicked: root.dashState.currentDate = new Date() + onRealCurrMonthChanged: handleDateChange() + onRealCurrYearChanged: handleDateChange() + + SequentialAnimation { + id: monthChangeAnim + + property int direction: 0 + + ScriptAction { + script: { + if (activeGrid === 1) { + titleTranslate1.x = -monthChangeAnim.direction * titleClip.width; + grid1Translate.x = -monthChangeAnim.direction * gridClip.width; + titleTranslate2.x = 0; + grid2Translate.x = 0; + } else { + titleTranslate2.x = -monthChangeAnim.direction * titleClip.width; + grid2Translate.x = -monthChangeAnim.direction * gridClip.width; + titleTranslate1.x = 0; + grid1Translate.x = 0; + } + } + } + + ParallelAnimation { + Anim { + property: "x" + target: titleTranslate1 + to: root.activeGrid === 1 ? 0 : monthChangeAnim.direction * titleClip.width + type: Anim.DefaultSpatial + } + + Anim { + property: "x" + target: grid1Translate + to: root.activeGrid === 1 ? 0 : monthChangeAnim.direction * gridClip.width + type: Anim.DefaultSpatial + } + + Anim { + property: "x" + target: titleTranslate2 + to: root.activeGrid === 2 ? 0 : monthChangeAnim.direction * titleClip.width + type: Anim.DefaultSpatial + } + + Anim { + property: "x" + target: grid2Translate + to: root.activeGrid === 2 ? 0 : monthChangeAnim.direction * gridClip.width + type: Anim.DefaultSpatial + } + } + } ColumnLayout { id: inner anchors.fill: parent anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small + spacing: Appearance.spacing.extraSmall RowLayout { id: monthNavigationRow Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Appearance.spacing.extraSmall - Item { - implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 - implicitWidth: implicitHeight + IconButton { + icon: "chevron_left" + padding: Appearance.padding.small + type: IconButton.Text - StateLayer { - id: prevMonthStateLayer - - radius: Appearance.rounding.full - - onClicked: { - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - } - } - - MaterialIcon { - id: prevMonthText - - anchors.centerIn: parent - color: DynamicColors.palette.m3tertiary - font.pointSize: Appearance.font.size.normal - font.weight: 700 - text: "chevron_left" - } + onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1) } Item { + Layout.fillHeight: true Layout.fillWidth: true - implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 - implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 + implicitHeight: monthYearDisplay1.implicitHeight + Appearance.padding.extraSmall * 2 + implicitWidth: monthYearDisplay1.implicitWidth + Appearance.padding.large * 2 StateLayer { - anchors.fill: monthYearDisplay - anchors.leftMargin: -Appearance.padding.normal - anchors.margins: -Appearance.padding.small - anchors.rightMargin: -Appearance.padding.normal + color: DynamicColors.palette.m3primary enabled: { const now = new Date(); - return root.currMonth !== now.getMonth() || root.currYear !== now.getFullYear(); + return root.realCurrMonth !== now.getMonth() || root.realCurrYear !== now.getFullYear(); } - radius: Appearance.rounding.full + radius: pressed ? Appearance.rounding.small : Appearance.rounding.large - onClicked: { - root.state.currentDate = new Date(); + Behavior on radius { + Anim { + type: Anim.DefaultEffects + } } + + onClicked: root.dashState.currentDate = new Date() } - CustomText { - id: monthYearDisplay + Item { + id: titleClip - anchors.centerIn: parent - color: DynamicColors.palette.m3primary - font.capitalization: Font.Capitalize - font.pointSize: Appearance.font.size.normal - font.weight: 500 - text: grid.title + anchors.fill: parent + clip: true + + CustomText { + id: monthYearDisplay1 + + anchors.centerIn: parent + // qmllint enable missing-property + color: DynamicColors.palette.m3primary + + // qmllint disable missing-property + text: grid1.item ? grid1.item.title : "" + visible: root.activeGrid === 1 || monthChangeAnim.running + + transform: Translate { + id: titleTranslate1 + } + } + + CustomText { + id: monthYearDisplay2 + + anchors.centerIn: parent + // qmllint enable missing-property + color: DynamicColors.palette.m3primary + + // qmllint disable missing-property + text: grid2.item ? grid2.item.title : "" + visible: root.activeGrid === 2 || monthChangeAnim.running + + transform: Translate { + id: titleTranslate2 + } + } } } - Item { - implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 - implicitWidth: implicitHeight + IconButton { + icon: "chevron_right" + padding: Appearance.padding.small + type: IconButton.Text - StateLayer { - id: nextMonthStateLayer - - radius: Appearance.rounding.full - - onClicked: { - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } - } - - MaterialIcon { - id: nextMonthText - - anchors.centerIn: parent - color: DynamicColors.palette.m3tertiary - font.pointSize: Appearance.font.size.normal - font.weight: 700 - text: "chevron_right" - } + onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1) } } @@ -130,109 +213,169 @@ CustomMouseArea { id: daysRow Layout.fillWidth: true - locale: grid.locale + locale: Qt.locale() delegate: CustomText { required property var model - color: (model.day === 0) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant - font.weight: 500 + color: (model.day === 0) ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3onSurface horizontalAlignment: Text.AlignHCenter text: model.shortName } } Item { + id: gridClip + Layout.fillWidth: true - implicitHeight: grid.implicitHeight + clip: true + implicitHeight: grid1.implicitHeight - MonthGrid { - id: grid + Component { + id: gridComp - anchors.fill: parent - locale: Qt.locale("en_SE") - month: root.currMonth - spacing: 3 - year: root.currYear + Item { + id: internalGridContainer - delegate: Item { - id: dayItem + property int month + property alias title: internalGrid.title + property int year - required property var model + implicitHeight: internalGrid.implicitHeight - implicitHeight: text.implicitHeight + Appearance.padding.small * 2 - implicitWidth: implicitHeight + MonthGrid { + id: internalGrid - CustomText { - id: text + anchors.fill: parent + locale: Qt.locale() + month: internalGridContainer.month + spacing: 3 + year: internalGridContainer.year - anchors.centerIn: parent - color: { - const dayOfWeek = dayItem.model.date.getUTCDay(); - if (dayOfWeek === 6) - return DynamicColors.palette.m3secondary; + delegate: Item { + id: dayItem - return DynamicColors.palette.m3onSurfaceVariant; + required property var model + + implicitHeight: text.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: implicitHeight + + CustomText { + id: text + + anchors.centerIn: parent + color: { + const dayOfWeek = dayItem.model.date.getDay(); + if (dayOfWeek === 0) + return DynamicColors.palette.m3tertiary; + + return DynamicColors.palette.m3onSurfaceVariant; + } + horizontalAlignment: Text.AlignHCenter + opacity: dayItem.model.today || dayItem.model.month === internalGrid.month ? 1 : 0.4 + text: internalGrid.locale.toString(dayItem.model.day) + } + } + } + + CustomRect { + id: todayIndicator + + property Item today + readonly property Item todayItem: internalGrid.contentItem.children.find(c => c.model.today) ?? null + + clip: true + color: DynamicColors.palette.m3primary + implicitHeight: width + implicitWidth: today ? (Math.max(today.implicitWidth, today.implicitHeight) - Appearance.padding.extraSmall) : 0 + opacity: todayItem ? 1 : 0 + radius: Appearance.rounding.full + scale: todayItem ? 1 : 0.7 + x: today ? today.x + (today.width - implicitWidth) / 2 : 0 + y: today ? today.y + Appearance.padding.extraSmall / 2 : 0 + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + Behavior on scale { + Anim { + type: Anim.FastSpatial + } + } + Behavior on x { + Anim { + } + } + Behavior on y { + Anim { + } + } + + onTodayItemChanged: { + if (todayItem) + today = todayItem; + } + + Coloriser { + colorizationColor: DynamicColors.palette.m3onPrimary + implicitHeight: internalGrid.height + implicitWidth: internalGrid.width + source: internalGrid + sourceColor: DynamicColors.palette.m3onSurface + x: -todayIndicator.x + y: -todayIndicator.y } - font.pointSize: Appearance.font.size.normal - font.weight: 500 - horizontalAlignment: Text.AlignHCenter - opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 - text: grid.locale.toString(dayItem.model.day) } } } - CustomRect { - id: todayIndicator + Loader { + id: grid1 - property Item today - readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null + anchors.fill: parent + sourceComponent: gridComp + visible: root.activeGrid === 1 || monthChangeAnim.running - clip: true - color: DynamicColors.palette.m3primary - implicitHeight: today?.implicitHeight ?? 0 - implicitWidth: today?.implicitWidth ?? 0 - opacity: todayItem ? 1 : 0 - radius: Appearance.rounding.full - scale: todayItem ? 1 : 0.7 - x: today ? today.x + (today.width - implicitWidth) / 2 : 0 - y: today?.y ?? 0 - - Behavior on opacity { - Anim { - } - } - Behavior on scale { - Anim { - } - } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + transform: Translate { + id: grid1Translate } - onTodayItemChanged: { - if (todayItem) - today = todayItem; + Binding { + property: "month" + target: grid1.item + value: root.month1 } - Coloriser { - colorizationColor: DynamicColors.palette.m3onPrimary - implicitHeight: grid.height - implicitWidth: grid.width - source: grid - sourceColor: DynamicColors.palette.m3onSurface - x: -todayIndicator.x - y: -todayIndicator.y + Binding { + property: "year" + target: grid1.item + value: root.year1 + } + } + + Loader { + id: grid2 + + anchors.fill: parent + sourceComponent: gridComp + visible: root.activeGrid === 2 || monthChangeAnim.running + + transform: Translate { + id: grid2Translate + } + + Binding { + property: "month" + target: grid2.item + value: root.month2 + } + + Binding { + property: "year" + target: grid2.item + value: root.year2 } } } diff --git a/Modules/Dashboard/Dash/User.qml b/Modules/Dashboard/Dash/User.qml index 80ee719..8109fb3 100644 --- a/Modules/Dashboard/Dash/User.qml +++ b/Modules/Dashboard/Dash/User.qml @@ -9,7 +9,7 @@ import QtQuick Row { id: root - required property PersistentProperties state + required property PersistentProperties dashState padding: 20 spacing: 12 diff --git a/Modules/Dashboard/Wrapper.qml b/Modules/Dashboard/Wrapper.qml index 791a31c..26da1e5 100644 --- a/Modules/Dashboard/Wrapper.qml +++ b/Modules/Dashboard/Wrapper.qml @@ -32,7 +32,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter sourceComponent: Content { - state: root.dashState + dashState: root.dashState visibilities: root.visibilities } }