hefty: bar: special ws indication

This commit is contained in:
end-4
2026-02-06 20:49:03 +01:00
parent ead98b98b8
commit ed8c8ae8d7
4 changed files with 302 additions and 222 deletions
@@ -282,6 +282,20 @@ Singleton {
} }
} }
property QtObject elementMoveSmall: QtObject {
property int duration: animationCurves.expressiveFastSpatialDuration
property int type: Easing.BezierSpline
property list<real> 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 QtObject elementMoveEnter: QtObject {
property int duration: 400 property int duration: 400
property int type: Easing.BezierSpline property int type: Easing.BezierSpline
@@ -8,12 +8,16 @@ NestableObject {
id: root id: root
required property HyprlandMonitor monitor required property HyprlandMonitor monitor
readonly property var liveMonitorData: HyprlandData.monitors.find(m => m.id === monitor.id)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
readonly property int activeWorkspace: monitor?.activeWorkspace?.id 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 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 fakeWorkspace: currentWorkspaceNotFake ? -9999 : activeWorkspace
readonly property int shownCount: C.Config.options.bar.workspaces.shown readonly property int shownCount: C.Config.options.bar.workspaces.shown
readonly property int group: Math.floor((activeWorkspace - 1) / shownCount) 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<bool> occupied: [] property list<bool> occupied: []
property list<var> biggestWindow: occupied.map((_, index) => { property list<var> biggestWindow: occupied.map((_, index) => {
@@ -0,0 +1,5 @@
import QtQuick
Rectangle {
radius: Math.min(width, height) / 2
}
@@ -31,6 +31,7 @@ Item {
property real workspaceIconOpacityShrinked: 1 property real workspaceIconOpacityShrinked: 1
property real workspaceIconMarginShrinked: -4 property real workspaceIconMarginShrinked: -4
property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % wsModel.shownCount property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % wsModel.shownCount
property real specialTextSize: workspaceButtonWidth * 0.5
Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter
Layout.fillWidth: vertical Layout.fillWidth: vertical
@@ -38,243 +39,264 @@ Item {
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : occupiedIndicators.implicitWidth implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : occupiedIndicators.implicitWidth
implicitHeight: vertical ? occupiedIndicators.implicitHeight : Appearance.sizes.barHeight implicitHeight: vertical ? occupiedIndicators.implicitHeight : Appearance.sizes.barHeight
/////////////////// Occupied indicators /////////////////// property real specialBlur: wsModel.specialWorkspaceActive ? 1 : 0
StyledRectangle { Behavior on specialBlur {
id: occupiedIndicatorsBg animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Item {
id: regularWorkspaces
anchors.fill: parent anchors.fill: parent
contentLayer: StyledRectangle.ContentLayer.Group
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
visible: false
}
WorkspaceLayout { scale: 1 - 0.08 * root.specialBlur
id: occupiedIndicators layer.smooth: true
anchors.centerIn: parent layer.enabled: root.specialBlur > 0
layer.effect: MultiEffect {
// rowSpacing: 0 brightness: -0.1 * root.specialBlur
// columnSpacing: 0 blurEnabled: true
// columns: root.vertical ? 1 : -1 blur: root.specialBlur
// rows: root.vertical ? -1 : 1 blurMax: 32
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);
} }
onPressed: Hyprland.dispatch(`workspace ${wsModel.getWorkspaceIdAt(hoverIndex)}`) /////////////////// Occupied indicators ///////////////////
StyledRectangle {
TrailingIndicator { id: occupiedIndicatorsBg
id: interactionIndicator anchors.fill: parent
index: interactionMouseArea.containsMouse ? interactionMouseArea.hoverIndex : root.workspaceIndexInGroup contentLayer: StyledRectangle.ContentLayer.Group
color: "transparent" color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
StateOverlay { visible: false
id: hoverOverlay
anchors.fill: interactionIndicator.indicatorRectangle
radius: root.activeWorkspaceSize / 2
hover: interactionMouseArea.containsMouse
press: interactionMouseArea.containsPress
contentColor: Appearance.colors.colPrimary
}
} }
}
/////////////////// Numbers /////////////////// WorkspaceLayout {
WorkspaceLayout { id: occupiedIndicators
id: numbersGrid anchors.centerIn: parent
z: 4
layer.enabled: true // For the masking
Repeater { layer.enabled: true
model: wsModel.shownCount visible: false
delegate: WorkspaceItem {
id: wsNum
property bool hasBiggestWindow: !!wsModel.biggestWindow[index]
property color contentColor: wsModel.occupied[wsNum.index] ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive
FadeLoader { Repeater {
shown: !(Config.options?.bar.workspaces.alwaysShowNumbers model: wsModel.shownCount
|| root.superPressAndHeld delegate: Item {
|| (Config.options?.bar.workspaces.showAppIcons && wsNum.hasBiggestWindow) id: wsBg
) required property int index
anchors.centerIn: parent readonly property int wsId: wsModel.getWorkspaceIdAt(index)
Circle { property bool currentOccupied: wsModel.occupied[index] && wsId != wsModel.fakeWorkspace
anchors.centerIn: parent property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (wsId - 1) != wsModel.fakeWorkspace
diameter: root.workspaceButtonWidth * 0.18 property bool nextOccupied: index < wsModel.shownCount - 1 && wsModel.occupied[index + 1] && (wsId + 1) != wsModel.fakeWorkspace
color: wsNum.contentColor implicitWidth: root.workspaceButtonWidth
} implicitHeight: root.workspaceButtonWidth
}
FadeLoader { // The idea: over-stretch to occupied sides, animate this for a smooth transition.
shown: root.superPressAndHeld // masking already prevents weird overlaps
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !wsNum.hasBiggestWindow || root.showNumbers)) Pill {
|| (root.superPressAndHeld && !Config.options?.bar.workspaces.showAppIcons) property real undirectionalWidth: root.workspaceButtonWidth * wsBg.currentOccupied
) property real undirectionalLength: root.workspaceButtonWidth * (1 + 0.5 * wsBg.previousOccupied + 0.5 * wsBg.nextOccupied) * currentOccupied
anchors.centerIn: parent property real undirectionalOffset: (!wsBg.currentOccupied ? 0.5 : -0.5 * wsBg.previousOccupied) * root.workspaceButtonWidth
StyledText { anchors.verticalCenter: root.vertical ? undefined : parent.verticalCenter
anchors.centerIn: parent anchors.horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
font { x: root.vertical ? 0 : undirectionalOffset
pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) y: root.vertical ? undirectionalOffset : 0
family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : defaultFont implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
Behavior on undirectionalWidth {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
} }
color: wsNum.contentColor Behavior on undirectionalLength {
text: wsNum.wsId 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 FadeLoader {
maskEnabled: true anchors.centerIn: parent
maskSource: numbersGrid shown: wsModel.specialWorkspaceActive
maskThresholdMin: 0.5 scale: 0.8 + 0.2 * root.specialBlur
maskSpreadAtMin: 1 // layer.enabled: true
} // layer.smooth: true
/////////////////// App icons /////////////////// Pill {
WorkspaceLayout { anchors.centerIn: parent
id: appsGrid property real undirectionalWidth: root.activeWorkspaceSize
z: 6 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 { implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
model: wsModel.shownCount implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
delegate: WorkspaceItem {
id: wsApp
property var biggestWindow: wsModel.biggestWindow[index]
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
AppIcon { StyledText {
id: appIcon id: specialWsText
property real cornerMargin: (!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? anchors.centerIn: parent
(root.workspaceButtonWidth - root.workspaceIconSize) / 2 : root.workspaceIconMarginShrinked text: (!root.vertical ? wsModel.specialWorkspaceName : "S")
anchors { color: Appearance.colors.colOnPrimary
bottom: parent.bottom font.pixelSize: root.specialTextSize
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 Behavior on undirectionalLength {
visible: false // Prevent dupe: the colorizer already copies the icon animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
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
}
} }
} }
} }
@@ -301,6 +323,41 @@ Item {
implicitHeight: root.vertical ? root.workspaceButtonWidth : Appearance.sizes.barHeight 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 { component TrailingIndicator: Item {
id: trailingIndicator id: trailingIndicator
anchors.fill: parent anchors.fill: parent
@@ -308,6 +365,11 @@ Item {
property alias indicatorRectangle: indicatorRect property alias indicatorRectangle: indicatorRect
property alias color: indicatorRect.color property alias color: indicatorRect.color
property var indexPair: AnimatedTabIndexPair {
id: idxPair
index: trailingIndicator.index
}
StyledRectangle { StyledRectangle {
id: indicatorRect id: indicatorRect
anchors { anchors {
@@ -315,11 +377,6 @@ Item {
horizontalCenter: vertical ? parent.horizontalCenter : undefined 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 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 indicatorLength: Math.abs(idxPair.idx1 - idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceSize
property real indicatorThickness: root.activeWorkspaceSize property real indicatorThickness: root.activeWorkspaceSize