diff --git a/dots/.config/quickshell/ii/modules/common/Appearance.qml b/dots/.config/quickshell/ii/modules/common/Appearance.qml index d64253476..34663f066 100644 --- a/dots/.config/quickshell/ii/modules/common/Appearance.qml +++ b/dots/.config/quickshell/ii/modules/common/Appearance.qml @@ -282,6 +282,20 @@ Singleton { } } + property QtObject elementMoveSmall: QtObject { + property int duration: animationCurves.expressiveFastSpatialDuration + property int type: Easing.BezierSpline + property list bezierCurve: animationCurves.expressiveFastSpatial + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveSmall.duration + easing.type: root.animation.elementMoveSmall.type + easing.bezierCurve: root.animation.elementMoveSmall.bezierCurve + } + } + } + property QtObject elementMoveEnter: QtObject { property int duration: 400 property int type: Easing.BezierSpline diff --git a/dots/.config/quickshell/ii/modules/common/models/WorkspaceModel.qml b/dots/.config/quickshell/ii/modules/common/models/WorkspaceModel.qml index 17b27c9f8..4490c8b00 100644 --- a/dots/.config/quickshell/ii/modules/common/models/WorkspaceModel.qml +++ b/dots/.config/quickshell/ii/modules/common/models/WorkspaceModel.qml @@ -8,12 +8,16 @@ NestableObject { id: root required property HyprlandMonitor monitor + readonly property var liveMonitorData: HyprlandData.monitors.find(m => m.id === monitor.id) readonly property Toplevel activeWindow: ToplevelManager.activeToplevel readonly property int activeWorkspace: monitor?.activeWorkspace?.id readonly property bool currentWorkspaceNotFake: activeWindow?.activated ?? false // Active empty workspace = fake. At least, that's how I like to call it. readonly property int fakeWorkspace: currentWorkspaceNotFake ? -9999 : activeWorkspace readonly property int shownCount: C.Config.options.bar.workspaces.shown readonly property int group: Math.floor((activeWorkspace - 1) / shownCount) + readonly property var specialWorkspace: liveMonitorData?.specialWorkspace + readonly property string specialWorkspaceName: specialWorkspace.name.replace("special:", "") + readonly property bool specialWorkspaceActive: specialWorkspaceName !== "" property list occupied: [] property list biggestWindow: occupied.map((_, index) => { diff --git a/dots/.config/quickshell/ii/modules/common/widgets/Pill.qml b/dots/.config/quickshell/ii/modules/common/widgets/Pill.qml new file mode 100644 index 000000000..7e9ae5d54 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/Pill.qml @@ -0,0 +1,5 @@ +import QtQuick + +Rectangle { + radius: Math.min(width, height) / 2 +} diff --git a/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HWorkspaces.qml b/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HWorkspaces.qml index e7895fd2e..059e365c4 100644 --- a/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HWorkspaces.qml +++ b/dots/.config/quickshell/ii/modules/hefty/topLayer/bar/widgets/HWorkspaces.qml @@ -31,6 +31,7 @@ Item { property real workspaceIconOpacityShrinked: 1 property real workspaceIconMarginShrinked: -4 property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % wsModel.shownCount + property real specialTextSize: workspaceButtonWidth * 0.5 Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter Layout.fillWidth: vertical @@ -38,243 +39,264 @@ Item { implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : occupiedIndicators.implicitWidth implicitHeight: vertical ? occupiedIndicators.implicitHeight : Appearance.sizes.barHeight - /////////////////// Occupied indicators /////////////////// - StyledRectangle { - id: occupiedIndicatorsBg + property real specialBlur: wsModel.specialWorkspaceActive ? 1 : 0 + Behavior on specialBlur { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + Item { + id: regularWorkspaces anchors.fill: parent - contentLayer: StyledRectangle.ContentLayer.Group - color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4) - visible: false - } - WorkspaceLayout { - id: occupiedIndicators - anchors.centerIn: parent - - // rowSpacing: 0 - // columnSpacing: 0 - // columns: root.vertical ? 1 : -1 - // rows: root.vertical ? -1 : 1 - - layer.enabled: true - visible: false - - Repeater { - model: wsModel.shownCount - delegate: Item { - id: wsBg - required property int index - readonly property int wsId: wsModel.getWorkspaceIdAt(index) - property bool currentOccupied: wsModel.occupied[index] && wsId != wsModel.fakeWorkspace - property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (wsId - 1) != wsModel.fakeWorkspace - property bool nextOccupied: index < wsModel.shownCount - 1 && wsModel.occupied[index + 1] && (wsId + 1) != wsModel.fakeWorkspace - implicitWidth: root.workspaceButtonWidth - implicitHeight: root.workspaceButtonWidth - - // The idea: over-stretch to occupied sides, animate this for a smooth transition. - // masking already prevents weird overlaps - Circle { - property real undirectionalWidth: root.workspaceButtonWidth * wsBg.currentOccupied - property real undirectionalLength: root.workspaceButtonWidth * (1 + 0.5 * wsBg.previousOccupied + 0.5 * wsBg.nextOccupied) * currentOccupied - property real undirectionalOffset: (!wsBg.currentOccupied ? 0.5 : -0.5 * wsBg.previousOccupied) * root.workspaceButtonWidth - radius: undirectionalWidth / 2 - anchors.verticalCenter: root.vertical ? undefined : parent.verticalCenter - anchors.horizontalCenter: root.vertical ? parent.horizontalCenter : undefined - x: root.vertical ? 0 : undirectionalOffset - y: root.vertical ? undirectionalOffset : 0 - implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength - implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth - - Behavior on undirectionalWidth { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - Behavior on undirectionalLength { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - Behavior on undirectionalOffset { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - } - } - } - } - - MaskMultiEffect { - id: occupiedIndicatorsMultiEffect - z: 1 - anchors.centerIn: parent - implicitWidth: occupiedIndicators.implicitWidth - implicitHeight: occupiedIndicators.implicitHeight - source: occupiedIndicatorsBg - maskSource: occupiedIndicators - } - - /////////////////// Active indicator /////////////////// - TrailingIndicator { - id: activeIndicator - anchors.fill: parent - z: 2 - - index: root.workspaceIndexInGroup - layer.enabled: true // For the masking - } - - /////////////////// Hover /////////////////// - MouseArea { - id: interactionMouseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - property int hoverIndex: { - const position = root.vertical ? mouseY : mouseX; - return Math.floor(position / root.workspaceButtonWidth); + scale: 1 - 0.08 * root.specialBlur + layer.smooth: true + layer.enabled: root.specialBlur > 0 + layer.effect: MultiEffect { + brightness: -0.1 * root.specialBlur + blurEnabled: true + blur: root.specialBlur + blurMax: 32 } - onPressed: Hyprland.dispatch(`workspace ${wsModel.getWorkspaceIdAt(hoverIndex)}`) - - TrailingIndicator { - id: interactionIndicator - index: interactionMouseArea.containsMouse ? interactionMouseArea.hoverIndex : root.workspaceIndexInGroup - color: "transparent" - StateOverlay { - id: hoverOverlay - anchors.fill: interactionIndicator.indicatorRectangle - radius: root.activeWorkspaceSize / 2 - hover: interactionMouseArea.containsMouse - press: interactionMouseArea.containsPress - contentColor: Appearance.colors.colPrimary - } + /////////////////// Occupied indicators /////////////////// + StyledRectangle { + id: occupiedIndicatorsBg + anchors.fill: parent + contentLayer: StyledRectangle.ContentLayer.Group + color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4) + visible: false } - } - /////////////////// Numbers /////////////////// - WorkspaceLayout { - id: numbersGrid - z: 4 - layer.enabled: true // For the masking + WorkspaceLayout { + id: occupiedIndicators + anchors.centerIn: parent - Repeater { - model: wsModel.shownCount - delegate: WorkspaceItem { - id: wsNum - property bool hasBiggestWindow: !!wsModel.biggestWindow[index] - property color contentColor: wsModel.occupied[wsNum.index] ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive + layer.enabled: true + visible: false - FadeLoader { - shown: !(Config.options?.bar.workspaces.alwaysShowNumbers - || root.superPressAndHeld - || (Config.options?.bar.workspaces.showAppIcons && wsNum.hasBiggestWindow) - ) - anchors.centerIn: parent - Circle { - anchors.centerIn: parent - diameter: root.workspaceButtonWidth * 0.18 - color: wsNum.contentColor - } - } - FadeLoader { - shown: root.superPressAndHeld - || ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !wsNum.hasBiggestWindow || root.showNumbers)) - || (root.superPressAndHeld && !Config.options?.bar.workspaces.showAppIcons) - ) - anchors.centerIn: parent - StyledText { - anchors.centerIn: parent - font { - pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) - family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : defaultFont + Repeater { + model: wsModel.shownCount + delegate: Item { + id: wsBg + required property int index + readonly property int wsId: wsModel.getWorkspaceIdAt(index) + property bool currentOccupied: wsModel.occupied[index] && wsId != wsModel.fakeWorkspace + property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (wsId - 1) != wsModel.fakeWorkspace + property bool nextOccupied: index < wsModel.shownCount - 1 && wsModel.occupied[index + 1] && (wsId + 1) != wsModel.fakeWorkspace + implicitWidth: root.workspaceButtonWidth + implicitHeight: root.workspaceButtonWidth + + // The idea: over-stretch to occupied sides, animate this for a smooth transition. + // masking already prevents weird overlaps + Pill { + property real undirectionalWidth: root.workspaceButtonWidth * wsBg.currentOccupied + property real undirectionalLength: root.workspaceButtonWidth * (1 + 0.5 * wsBg.previousOccupied + 0.5 * wsBg.nextOccupied) * currentOccupied + property real undirectionalOffset: (!wsBg.currentOccupied ? 0.5 : -0.5 * wsBg.previousOccupied) * root.workspaceButtonWidth + anchors.verticalCenter: root.vertical ? undefined : parent.verticalCenter + anchors.horizontalCenter: root.vertical ? parent.horizontalCenter : undefined + x: root.vertical ? 0 : undirectionalOffset + y: root.vertical ? undirectionalOffset : 0 + implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength + implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth + + Behavior on undirectionalWidth { + animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this) } - color: wsNum.contentColor - text: wsNum.wsId + Behavior on undirectionalLength { + animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this) + } + Behavior on undirectionalOffset { + animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this) + } + } + } + } + } + + MaskMultiEffect { + id: occupiedIndicatorsMultiEffect + z: 1 + anchors.centerIn: parent + implicitWidth: occupiedIndicators.implicitWidth + implicitHeight: occupiedIndicators.implicitHeight + source: occupiedIndicatorsBg + maskSource: occupiedIndicators + } + + /////////////////// Active indicator /////////////////// + TrailingIndicator { + id: activeIndicator + anchors.fill: parent + z: 2 + + index: root.workspaceIndexInGroup + } + + /////////////////// Hover /////////////////// + MouseArea { + id: interactionMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + property int hoverIndex: { + const position = root.vertical ? mouseY : mouseX; + return Math.floor(position / root.workspaceButtonWidth); + } + + onPressed: Hyprland.dispatch(`workspace ${wsModel.getWorkspaceIdAt(hoverIndex)}`) + onWheel: (event) => { + if (event.angleDelta.y < 0) + Hyprland.dispatch(`workspace r+1`); + else if (event.angleDelta.y > 0) + Hyprland.dispatch(`workspace r-1`); + } + + TrailingIndicator { + id: interactionIndicator + index: interactionMouseArea.containsMouse ? interactionMouseArea.hoverIndex : root.workspaceIndexInGroup + color: "transparent" + StateOverlay { + id: hoverOverlay + anchors.fill: interactionIndicator.indicatorRectangle + radius: root.activeWorkspaceSize / 2 + hover: interactionMouseArea.containsMouse + press: interactionMouseArea.containsPress + contentColor: Appearance.colors.colPrimary + } + } + } + + /////////////////// Numbers /////////////////// + WorkspaceLayout { + id: numbersGrid + z: 4 + layer.enabled: true // For the masking + + Repeater { + model: wsModel.shownCount + delegate: NumberWorkspaceItem {} + } + } + Colorizer { + z: 5 + anchors.fill: numbersGrid + colorizationColor: Appearance.colors.colOnPrimary + sourceColor: Appearance.colors.colOnSecondaryContainer + + source: activeIndicator + maskEnabled: true + maskSource: numbersGrid + + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + /////////////////// App icons /////////////////// + WorkspaceLayout { + id: appsGrid + z: 6 + + Repeater { + model: wsModel.shownCount + delegate: WorkspaceItem { + id: wsApp + property var biggestWindow: wsModel.biggestWindow[index] + property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing") + + AppIcon { + id: appIcon + property real cornerMargin: (!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? + (root.workspaceButtonWidth - root.workspaceIconSize) / 2 : root.workspaceIconMarginShrinked + anchors { + bottom: parent.bottom + right: parent.right + bottomMargin: (parent.implicitHeight - root.workspaceButtonWidth) / 2 + cornerMargin + rightMargin: (parent.implicitWidth - root.workspaceButtonWidth) / 2 + cornerMargin + } + + animated: !wsApp.biggestWindow // Prevent the "image-missing" icon + visible: false // Prevent dupe: the colorizer already copies the icon + + source: wsApp.mainAppIconSource + implicitSize: NumberUtils.roundToEven((!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? root.workspaceIconSize : root.workspaceIconSizeShrinked) + + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on cornerMargin { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on implicitSize { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + + Circle { + id: iconMask + visible: false + layer.enabled: true + diameter: appIcon.implicitSize + } + + Colorizer { + anchors.fill: appIcon + implicitWidth: appIcon.implicitWidth + implicitHeight: appIcon.implicitHeight + colorizationColor: Appearance.m3colors.darkmode ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary + colorization: Config.options.bar.workspaces.monochromeIcons ? 0.8 : 0.5 + brightness: 0 + source: appIcon + + opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 : + (wsApp.biggestWindow && !root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? + 1 : wsApp.biggestWindow ? root.workspaceIconOpacityShrinked : 0 + visible: opacity > 0 + + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + maskEnabled: true + maskSource: iconMask + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 } } } } } - Colorizer { - z: 5 - anchors.fill: numbersGrid - colorizationColor: Appearance.colors.colOnPrimary - sourceColor: Appearance.colors.colOnSecondaryContainer - source: activeIndicator - maskEnabled: true - maskSource: numbersGrid + FadeLoader { + anchors.centerIn: parent + shown: wsModel.specialWorkspaceActive - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } + scale: 0.8 + 0.2 * root.specialBlur + // layer.enabled: true + // layer.smooth: true - /////////////////// App icons /////////////////// - WorkspaceLayout { - id: appsGrid - z: 6 + Pill { + anchors.centerIn: parent + property real undirectionalWidth: root.activeWorkspaceSize + property real undirectionalLength: { + const base = root.workspaceButtonWidth * Math.min(1.35, wsModel.shownCount) // Who tf only configures only 2 workspaces shown anyway? + if (root.vertical) return base; + return specialWsText.implicitWidth + undirectionalWidth + } + color: Appearance.colors.colPrimary - Repeater { - model: wsModel.shownCount - delegate: WorkspaceItem { - id: wsApp - property var biggestWindow: wsModel.biggestWindow[index] - property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing") + implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength + implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth - AppIcon { - id: appIcon - property real cornerMargin: (!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? - (root.workspaceButtonWidth - root.workspaceIconSize) / 2 : root.workspaceIconMarginShrinked - anchors { - bottom: parent.bottom - right: parent.right - bottomMargin: (parent.implicitHeight - root.workspaceButtonWidth) / 2 + cornerMargin - rightMargin: (parent.implicitWidth - root.workspaceButtonWidth) / 2 + cornerMargin - } + StyledText { + id: specialWsText + anchors.centerIn: parent + text: (!root.vertical ? wsModel.specialWorkspaceName : "S") + color: Appearance.colors.colOnPrimary + font.pixelSize: root.specialTextSize + } - animated: !wsApp.biggestWindow // Prevent the "image-missing" icon - visible: false // Prevent dupe: the colorizer already copies the icon - - source: wsApp.mainAppIconSource - implicitSize: NumberUtils.roundToEven((!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? root.workspaceIconSize : root.workspaceIconSizeShrinked) - - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - Behavior on cornerMargin { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - Behavior on implicitSize { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - } - - Circle { - id: iconMask - visible: false - layer.enabled: true - diameter: appIcon.implicitSize - } - - Colorizer { - anchors.fill: appIcon - implicitWidth: appIcon.implicitWidth - implicitHeight: appIcon.implicitHeight - colorizationColor: Appearance.colors.colOnSecondaryContainer - colorization: Config.options.bar.workspaces.monochromeIcons * 0.7 - brightness: 0 - source: appIcon - - opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 : - (wsApp.biggestWindow && !root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? - 1 : wsApp.biggestWindow ? root.workspaceIconOpacityShrinked : 0 - visible: opacity > 0 - - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - maskEnabled: true - maskSource: iconMask - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } + Behavior on undirectionalLength { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } } } @@ -301,6 +323,41 @@ Item { implicitHeight: root.vertical ? root.workspaceButtonWidth : Appearance.sizes.barHeight } + component NumberWorkspaceItem: WorkspaceItem { + id: wsNum + property bool hasBiggestWindow: !!wsModel.biggestWindow[index] + property color contentColor: wsModel.occupied[wsNum.index] ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive + + FadeLoader { + shown: !(Config.options?.bar.workspaces.alwaysShowNumbers + || root.superPressAndHeld + || (Config.options?.bar.workspaces.showAppIcons && wsNum.hasBiggestWindow) + ) + anchors.centerIn: parent + Circle { + anchors.centerIn: parent + diameter: root.workspaceButtonWidth * 0.18 + color: wsNum.contentColor + } + } + FadeLoader { + shown: root.superPressAndHeld + || ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !wsNum.hasBiggestWindow || root.showNumbers)) + || (root.superPressAndHeld && !Config.options?.bar.workspaces.showAppIcons) + ) + anchors.centerIn: parent + StyledText { + anchors.centerIn: parent + font { + pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) + family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : defaultFont + } + color: wsNum.contentColor + text: wsNum.wsId + } + } + } + component TrailingIndicator: Item { id: trailingIndicator anchors.fill: parent @@ -308,6 +365,11 @@ Item { property alias indicatorRectangle: indicatorRect property alias color: indicatorRect.color + property var indexPair: AnimatedTabIndexPair { + id: idxPair + index: trailingIndicator.index + } + StyledRectangle { id: indicatorRect anchors { @@ -315,11 +377,6 @@ Item { horizontalCenter: vertical ? parent.horizontalCenter : undefined } - AnimatedTabIndexPair { - id: idxPair - index: trailingIndicator.index - } - property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin property real indicatorLength: Math.abs(idxPair.idx1 - idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceSize property real indicatorThickness: root.activeWorkspaceSize