diff --git a/components/StateLayer.qml b/components/StateLayer.qml index ee9ca9fb..fe7a2b95 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -9,6 +9,7 @@ MouseArea { property bool disabled property bool showHoverBackground: true + property bool manualPressOverride readonly property alias rect: base property bool shapeMorph @@ -60,12 +61,17 @@ MouseArea { onPressed: e => press(e.x, e.y) onPressedChanged: { - if (!pressed && !rippleAnim.running && circle.opacity > 0) + if (!(pressed || manualPressOverride) && !rippleAnim.running && circle.opacity > 0) + fadeAnim.start(); + } + + onManualPressOverrideChanged: { + if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) fadeAnim.start(); } onCircleRadiusChanged: { - if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) + if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) fadeAnim.start(); } diff --git a/modules/nexus/Nexus.qml b/modules/nexus/Nexus.qml index 734ddfbe..b22d00c3 100644 --- a/modules/nexus/Nexus.qml +++ b/modules/nexus/Nexus.qml @@ -89,4 +89,15 @@ Item { nState: nState width: Math.min(Tokens.sizes.nexus.maxNavWidth, Math.round(root.width / 3)) } + + Pages { + anchors.left: navPane.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: navPane.anchors.margins + anchors.margins + anchors.margins: Tokens.padding.extraLarge + + nState: nState + } } diff --git a/modules/nexus/Pages.qml b/modules/nexus/Pages.qml new file mode 100644 index 00000000..25e01bac --- /dev/null +++ b/modules/nexus/Pages.qml @@ -0,0 +1,111 @@ +pragma ComponentBehavior: Bound + +import "pages" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus + +Item { + id: root + + required property NexusState nState + + readonly property list pageComps: [ + Component { + WallpaperAndStyle { + nState: root.nState + } + } + ] + + property int lastPageIdx + property int animOff + + Loader { + id: loader + + anchors.fill: parent + layer.enabled: opacity < 1 + Component.onCompleted: sourceComponent = root.pageComps[root.nState.currentPageIdx] ?? placeholderComp + } + + Connections { + function onCurrentPageIdxChanged(): void { + switchAnim.complete(); + root.animOff = root.Tokens.padding.extraLarge * (root.nState.currentPageIdx > root.lastPageIdx ? -1 : 1); + switchAnim.start(); + root.lastPageIdx = root.nState.currentPageIdx; + } + + target: root.nState + } + + SequentialAnimation { + id: switchAnim + + Anim { + target: loader + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + PropertyAction { + target: loader + property: "sourceComponent" + value: root.pageComps[root.nState.currentPageIdx] ?? placeholderComp + } + PropertyAction { + target: loader.anchors + property: "topMargin" + value: root.animOff + } + PropertyAction { + target: loader.anchors + property: "bottomMargin" + value: -root.animOff + } + ParallelAnimation { + Anim { + target: loader + property: "opacity" + from: 0 + to: 1 + type: Anim.SlowEffects + } + Anim { + target: loader.anchors + properties: "topMargin,bottomMargin" + to: 0 + type: Anim.SlowEffects + } + } + } + + Component { + id: placeholderComp + + Item { + ColumnLayout { + anchors.centerIn: parent + spacing: Tokens.padding.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "handyman" + color: Colours.palette.m3outlineVariant + font: Tokens.font.icon.extraLarge + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Page under construction") + color: Colours.palette.m3outlineVariant + font: Tokens.font.title.large + } + } + } + } +} diff --git a/modules/nexus/common/ToggleRow.qml b/modules/nexus/common/ToggleRow.qml new file mode 100644 index 00000000..8caf35ec --- /dev/null +++ b/modules/nexus/common/ToggleRow.qml @@ -0,0 +1,55 @@ +import QtQuick +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledSwitch { + id: root + + property bool first + property bool last + + implicitWidth: implicitContentWidth + implicitIndicatorWidth + Tokens.padding.largeIncreased * 2 + implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + Tokens.padding.medium * 2 + cLayer: 2 + + indicator.anchors.verticalCenter: verticalCenter + indicator.anchors.right: right + indicator.anchors.rightMargin: Tokens.padding.large + + onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY) + + background: StyledRect { + color: Colours.tPalette.m3surfaceContainer + topLeftRadius: root.first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + topRightRadius: root.first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + bottomLeftRadius: root.last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + bottomRightRadius: root.last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall + + StateLayer { + id: stateLayer + + manualPressOverride: root.pressed + + topLeftRadius: parent.topLeftRadius + topRightRadius: parent.topRightRadius + bottomLeftRadius: parent.bottomLeftRadius + bottomRightRadius: parent.bottomRightRadius + } + } + + contentItem: StyledText { + id: label + + anchors.left: parent.left + anchors.right: root.indicator.left + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.spacing.medium + + text: root.text + font: Tokens.font.body.small + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } +} diff --git a/modules/nexus/pages/WallpaperAndStyle.qml b/modules/nexus/pages/WallpaperAndStyle.qml new file mode 100644 index 00000000..14437385 --- /dev/null +++ b/modules/nexus/pages/WallpaperAndStyle.qml @@ -0,0 +1,160 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Components +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services +import qs.modules.nexus +import qs.modules.nexus.common + +ColumnLayout { + id: root + + required property NexusState nState + readonly property int cappedWidth: Math.min(800, width) + + spacing: Tokens.spacing.large + + StyledText { + text: qsTr("Wallpaper & style") + font: Tokens.font.title.large + } + + StyledClippingRect { + id: wallWrapper + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Tokens.spacing.extraLarge + implicitWidth: { + const screen = root.nState.screen; + return implicitHeight / screen.height * screen.width; + } + implicitHeight: { + const screen = root.nState.screen; + const cWidth = root.cappedWidth; + return Math.min(Math.round(cWidth * 0.4), cWidth / screen.width * screen.height); + } + + color: Colours.palette.m3surfaceContainer + radius: Tokens.rounding.large + + Loader { + anchors.centerIn: parent + + opacity: wallImg.status === Image.Ready ? 0 : 1 + active: opacity > 0 + + sourceComponent: StyledRect { + implicitWidth: wallLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2 + implicitHeight: wallLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2 + + color: Colours.palette.m3primaryContainer + radius: Tokens.rounding.full + + LoadingIndicator { + id: wallLoadingIndicator + + anchors.centerIn: parent + containsIcon: true + implicitSize: wallWrapper.implicitHeight * 0.4 + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + Image { + id: wallImg + + anchors.fill: parent + source: Wallpapers.current + asynchronous: true + fillMode: Image.PreserveAspectCrop + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + retainWhileLoading: true + opacity: status === Image.Ready ? 1 : 0 + + Behavior on opacity { + Anim { + type: Anim.SlowEffects + } + } + } + } + + ButtonRow { + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.small + + IconTextButton { + icon: "wallpaper" + text: qsTr("Wallpapers") + font: Tokens.font.body.large + isRound: true + shapeMorph: true + type: IconTextButton.Tonal + horizontalPadding: Tokens.padding.extraLarge + verticalPadding: Tokens.padding.medium + onClicked: ; // TODO + } + + IconTextButton { + icon: "palette" + text: qsTr("Colours") + font: Tokens.font.body.large + isRound: true + shapeMorph: true + type: IconTextButton.Tonal + horizontalPadding: Tokens.padding.extraLarge + verticalPadding: Tokens.padding.medium + onClicked: ; // TODO + } + } + + ToggleRow { + Layout.alignment: Qt.AlignHCenter + implicitWidth: root.cappedWidth + + first: true + text: qsTr("Display wallpaper") + checked: Config.background.wallpaperEnabled + onToggled: GlobalConfig.background.wallpaperEnabled = checked + } + + ToggleRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + Layout.alignment: Qt.AlignHCenter + implicitWidth: root.cappedWidth + + text: qsTr("Dark theme") + checked: !Colours.light + onToggled: Colours.setMode(checked ? "dark" : "light") + } + + ToggleRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + Layout.alignment: Qt.AlignHCenter + implicitWidth: root.cappedWidth + + last: true + text: qsTr("Transparency") + checked: Colours.transparency.enabled + onToggled: GlobalConfig.appearance.transparency.enabled = checked + } + + Item { + Layout.fillHeight: true + } +}