bar: workspaces from hefty

This commit is contained in:
end-4
2026-07-16 11:42:32 +07:00
parent c04b0bbc81
commit 446504ad42
12 changed files with 650 additions and 273 deletions
@@ -0,0 +1,16 @@
pragma Singleton
import Quickshell
Singleton {
id: root
/**
* Rounds the given number to the nearest even integer.
*
* @param {number} num - The number to round.
* @returns {number} The nearest even integer.
*/
function roundToEven(num) {
return Math.round(num / 2) * 2;
}
}
@@ -0,0 +1,63 @@
import QtQuick
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.services
import qs.modules.common as C
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 ?? 1
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:", "") ?? "special"
readonly property bool specialWorkspaceActive: specialWorkspaceName !== ""
property list<bool> occupied: []
property list<var> biggestWindow: occupied.map((_, index) => {
const wsId = getWorkspaceIdAt(index);
var biggestWindow = HyprlandData.biggestWindowForWorkspace(wsId);
return biggestWindow;
})
function getWorkspaceId(group, index) {
return group * root.shownCount + index + 1;
}
function getWorkspaceIdAt(index) {
return root.getWorkspaceId(root.group, index);
}
// Function to update workspaceOccupied
function updateWorkspaceOccupied() {
root.occupied = Array.from({
length: root.shownCount
}, (_, i) => {
const thisWorkspaceId = getWorkspaceId(root.group, i);
return Hyprland.workspaces.values.some(ws => ws.id === thisWorkspaceId);
});
}
// Occupied workspace updates
Component.onCompleted: updateWorkspaceOccupied()
Connections {
target: Hyprland.workspaces
function onValuesChanged() {
root.updateWorkspaceOccupied();
}
}
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() {
root.updateWorkspaceOccupied();
}
}
onGroupChanged: {
updateWorkspaceOccupied();
}
}
@@ -0,0 +1,15 @@
import QtQuick
import org.kde.kirigami as Kirigami
import qs.services
import qs.modules.common
Kirigami.Icon {
id: root
property real implicitSize: 26
implicitWidth: implicitSize
implicitHeight: implicitSize
roundToIconSize: false
animated: true // It's just fading from one icon to another
}
@@ -0,0 +1,16 @@
pragma ComponentBehavior: Bound
import QtQuick
// A type that's both capable of being rows and columns
// Qt Row is just a locked down Grid smh
// Calling it a Box because that's how row-or-column widget is called in Gtk
Grid {
id: root
property bool vertical: false
columns: vertical ? 1 : -1
rows: vertical ? -1 : 1
property alias spacing: root.rowSpacing
columnSpacing: rowSpacing
}
@@ -0,0 +1,18 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
// Box, Layout version
// A type that's both capable of being rows and columns
// Qt Row is just a locked down Grid smh
// Calling it a Box because that's how row-or-column widget is called in Gtk
GridLayout {
id: root
property bool vertical: false
columns: vertical ? 1 : -1
rows: vertical ? -1 : 1
property alias spacing: root.rowSpacing
columnSpacing: rowSpacing
}
@@ -0,0 +1,9 @@
pragma ComponentBehavior: Bound
import QtQuick
// MouseArea that contains good defaults for buttons
MouseArea {
id: root
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Effects
import qs.modules.common
MultiEffect {
property color sourceColor: "black"
colorization: 1
brightness: 1 - sourceColor.hslLightness
Behavior on colorizationColor {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
@@ -0,0 +1,5 @@
import QtQuick
Rectangle {
radius: Math.min(width, height) / 2
}
@@ -0,0 +1,19 @@
import QtQuick
Rectangle {
id: root
// https://m3.material.io/foundations/interaction/states/state-layers
enum State {
Hover, Focus, Press, Drag
}
property var state: StateLayer.State.Hover
opacity: switch(state) {
case StateLayer.State.Hover: return 0.08;
case StateLayer.State.Focus: return 0.1;
case StateLayer.State.Press: return 0.1;
case StateLayer.State.Drag: return 0.16;
default: return 0;
}
}
@@ -0,0 +1,66 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.common as C
Rectangle {
id: root
property bool hover: false
property bool press: false
property bool drag: false
property color contentColor: C.Appearance.m3colors.m3onBackground
color: "transparent"
FadeLoader {
id: hoverLoader
anchors.fill: parent
shown: root.hover
sourceComponent: StateLayer {
state: StateLayer.State.Hover
color: root.contentColor
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
}
FadeLoader {
id: focusLoader
anchors.fill: parent
shown: root.focus
sourceComponent: StateLayer {
state: StateLayer.State.Focus
color: root.contentColor
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
}
FadeLoader {
id: pressLoader
anchors.fill: parent
shown: root.press
sourceComponent: StateLayer {
state: StateLayer.State.Press
color: root.contentColor
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
}
FadeLoader {
id: dragLoader
anchors.fill: parent
shown: root.drag
sourceComponent: StateLayer {
state: StateLayer.State.Drag
color: root.contentColor
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
}
}
}
@@ -0,0 +1,22 @@
import QtQuick
import qs.modules.common as C
// This is to enable future fancy styles for rectangles. Some ideas:
// - normal rounded rect
// - osk.sh
// - 3d
// i hope i actually get to this and not shrimply forget
// aaaaa i realized for this to work i would have to make this for shapes in general not just rects
Rectangle {
enum ContentLayer { Background, Pane, Group, Subgroup, Control }
property var contentLayer: StyledRectangle.ContentLayer.Pane // To appropriately add effects like shadows/3d-ization
color: switch(contentLayer) {
case StyledRectangle.ContentLayer.Background: C.Appearance.colors.colLayer0;
case StyledRectangle.ContentLayer.Pane: C.Appearance.colors.colLayer1;
case StyledRectangle.ContentLayer.Group: C.Appearance.colors.colLayer2;
case StyledRectangle.ContentLayer.Subgroup: C.Appearance.colors.colLayer3;
case StyledRectangle.ContentLayer.Control: C.Appearance.colors.colLayer4;
default: C.Appearance.colors.colLayer1;
}
}
@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
@@ -5,316 +6,429 @@ import qs.modules.common.models
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
Item {
ButtonMouseArea {
id: root
property bool vertical: false
property bool borderless: Config.options.bar.borderless
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
readonly property int workspacesShown: Config.options.bar.workspaces.shown
readonly property int workspaceGroup: Math.floor((effectiveActiveWorkspaceId - 1) / root.workspacesShown)
property list<bool> workspaceOccupied: []
property int widgetPadding: 4
property int workspaceButtonWidth: 26
WorkspaceModel {
id: wsModel
monitor: root.monitor
}
property bool vertical: Config.options.bar.vertical
property bool superPressAndHeld: false // Relevant modifications at bottom of file
property real workspaceButtonWidth: 26
property real activeWorkspaceMargin: 2
property real activeWorkspaceSize: workspaceButtonWidth - activeWorkspaceMargin * 2
property real workspaceIconSize: workspaceButtonWidth * 0.69
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
property real workspaceIconOpacityShrinked: 1
property real workspaceIconMarginShrinked: -4
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % wsModel.shownCount
property real specialTextSize: workspaceButtonWidth * 0.5
property bool showNumbers: false
Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter
Layout.fillWidth: vertical
Layout.fillHeight: !vertical
readonly property real barThickness: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
implicitWidth: vertical ? barThickness : occupiedIndicators.implicitWidth
implicitHeight: vertical ? occupiedIndicators.implicitHeight : barThickness
property real specialBlur: (wsModel.specialWorkspaceActive && !containsMouse) ? 1 : 0
Behavior on specialBlur {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
// Interactions
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
property int hoverIndex: {
const position = root.vertical ? mouseY : mouseX;
return Math.floor(position / root.workspaceButtonWidth);
}
function switchWorkspaceToHovered() {
Hyprland.dispatch(`hl.dsp.focus({workspace = ${wsModel.getWorkspaceIdAt(hoverIndex)}})`);
}
onPressed: mouse => {
if (mouse.button == Qt.LeftButton)
switchWorkspaceToHovered();
else if (mouse.button == Qt.RightButton)
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
onWheel: event => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`hl.dsp.focus({workspace = "r+1"})`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`hl.dsp.focus({workspace = "r-1"})`);
}
// Indications
Item {
id: regularWorkspaces
anchors.fill: parent
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
}
/////////////////// Occupied indicators ///////////////////
StyledRectangle {
id: occupiedIndicatorsBg
anchors.fill: parent
contentLayer: StyledRectangle.ContentLayer.Group
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
visible: false
}
WorkspaceLayout {
id: occupiedIndicators
anchors.centerIn: parent
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
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)
}
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 ///////////////////
TrailingIndicator {
id: interactionIndicator
z: 3
index: root.containsMouse ? root.hoverIndex : root.workspaceIndexInGroup
color: "transparent"
StateOverlay {
id: hoverOverlay
anchors.fill: interactionIndicator.indicatorRectangle
radius: root.activeWorkspaceSize / 2
hover: root.containsMouse
press: root.containsPress
drag: true // There are too many layers so we need to force this to be a lil more opaque
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 && wsApp.biggestWindow) ? (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.workspaceIconSize)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on cornerMargin {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
}
Circle {
id: iconMask
visible: false
layer.enabled: true
diameter: appIcon.implicitSize
}
Loader { // Somehow putting this multieffect in a loader prevents it from not showing up
id: colorizer
anchors.fill: appIcon
sourceComponent: Colorizer {
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
scale: ((!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? root.workspaceIconSize : root.workspaceIconSizeShrinked) / root.workspaceIconSize
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on scale {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
maskEnabled: true
maskSource: iconMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
}
}
}
}
FadeLoader {
anchors.centerIn: parent
shown: wsModel.specialWorkspaceActive
scale: 0.8 + 0.2 * root.specialBlur
opacity: root.specialBlur
Behavior on opacity {} // Don't animate, as specialBlur is already animated
sourceComponent: 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
implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
StyledText {
id: specialWsText
anchors.centerIn: parent
text: (!root.vertical ? wsModel.specialWorkspaceName : "S")
color: Appearance.colors.colOnPrimary
font.pixelSize: root.specialTextSize
}
Behavior on undirectionalLength {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
/////////////////// Super key press handling ///////////////////
Timer {
id: showNumbersTimer
id: superPressAndHeldTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.showNumbers = true
root.superPressAndHeld = true;
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showNumbersTimer.restart();
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable)
return;
if (GlobalStates.superDown)
superPressAndHeldTimer.restart();
else {
showNumbersTimer.stop();
root.showNumbers = false;
superPressAndHeldTimer.stop();
root.superPressAndHeld = false;
}
}
function onSuperReleaseMightTriggerChanged() {
showNumbersTimer.stop()
function onSuperReleaseMightTriggerChanged() {
superPressAndHeldTimer.stop();
}
}
// Function to update workspaceOccupied
function updateWorkspaceOccupied() {
workspaceOccupied = Array.from({ length: root.workspacesShown }, (_, i) => {
return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * root.workspacesShown + i + 1);
})
}
// Occupied workspace updates
Component.onCompleted: updateWorkspaceOccupied()
Connections {
target: Hyprland.workspaces
function onValuesChanged() {
updateWorkspaceOccupied();
}
}
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() {
updateWorkspaceOccupied();
}
}
onWorkspaceGroupChanged: {
updateWorkspaceOccupied();
}
implicitWidth: root.vertical ? Appearance.sizes.verticalBarWidth : (root.workspaceButtonWidth * root.workspacesShown)
implicitHeight: root.vertical ? (root.workspaceButtonWidth * root.workspacesShown) : Appearance.sizes.barHeight
// Scroll to switch workspaces
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`hl.dsp.focus({workspace = "r+1"})`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`hl.dsp.focus({workspace = "r-1"})`);
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onPressed: (event) => {
if (event.button === Qt.BackButton) {
Hyprland.dispatch(`hl.dsp.workspace.toggle_special("special")`);
}
}
}
// Workspaces - background
Grid {
z: 1
anchors.centerIn: parent
rowSpacing: 0
columnSpacing: 0
columns: root.vertical ? 1 : root.workspacesShown
rows: root.vertical ? root.workspacesShown : 1
Repeater {
model: root.workspacesShown
Rectangle {
z: 1
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
radius: (width / 2)
property var previousOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === index))
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === index+2))
property var radiusPrev: previousOccupied ? 0 : (width / 2)
property var radiusNext: rightOccupied ? 0 : (width / 2)
topLeftRadius: radiusPrev
bottomLeftRadius: root.vertical ? radiusNext : radiusPrev
topRightRadius: root.vertical ? radiusPrev : radiusNext
bottomRightRadius: radiusNext
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === index+1)) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusPrev {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusNext {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
}
// Active workspace
Rectangle {
z: 2
// Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight
radius: Appearance.rounding.full
color: Appearance.colors.colPrimary
component WorkspaceLayout: Box {
anchors {
verticalCenter: vertical ? undefined : parent.verticalCenter
horizontalCenter: vertical ? parent.horizontalCenter : undefined
top: !root.vertical ? parent.top : undefined
bottom: !root.vertical ? parent.bottom : undefined
left: root.vertical ? parent.left : undefined
right: root.vertical ? parent.right : undefined
}
AnimatedTabIndexPair {
id: idxPair
index: root.workspaceIndexInGroup
}
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorLength: Math.abs(idxPair.idx1 - idxPair.idx2) * workspaceButtonWidth + workspaceButtonWidth - root.activeWorkspaceMargin * 2
property real indicatorThickness: workspaceButtonWidth - root.activeWorkspaceMargin * 2
x: root.vertical ? null : indicatorPosition
implicitWidth: root.vertical ? indicatorThickness : indicatorLength
y: root.vertical ? indicatorPosition : null
implicitHeight: root.vertical ? indicatorLength : indicatorThickness
rowSpacing: 0
columnSpacing: 0
vertical: root.vertical
}
// Workspaces - numbers
Grid {
z: 3
component WorkspaceItem: Item {
required property int index
readonly property int wsId: wsModel.getWorkspaceIdAt(index)
implicitWidth: root.vertical ? root.barThickness : root.workspaceButtonWidth
implicitHeight: root.vertical ? root.workspaceButtonWidth : root.barThickness
}
columns: root.vertical ? 1 : root.workspacesShown
rows: root.vertical ? root.workspacesShown : 1
columnSpacing: 0
rowSpacing: 0
component NumberWorkspaceItem: WorkspaceItem {
id: wsNum
property bool hasBiggestWindow: !!wsModel.biggestWindow[index]
property int wsId: wsModel.getWorkspaceIdAt(index)
property color contentColor: (wsModel.occupied[wsNum.index] && wsId !== wsModel.fakeWorkspace) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive
property bool showingNumbers: {
if (root.superPressAndHeld)
return true;
if (GlobalStates.screenLocked)
return false;
if (Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !wsNum.hasBiggestWindow))
return true;
return false;
}
anchors.fill: parent
Repeater {
model: root.workspacesShown
Button {
id: button
property int workspaceValue: workspaceGroup * root.workspacesShown + index + 1
implicitHeight: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.verticalBarWidth
onPressed: Hyprland.dispatch(`hl.dsp.focus({ workspace = ${workspaceValue}})`)
width: vertical ? undefined : root.workspaceButtonWidth
height: vertical ? root.workspaceButtonWidth : undefined
background: Item {
id: workspaceButtonBackground
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(button.workspaceValue)
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
StyledText { // Workspace number text
opacity: root.showNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|| (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
) ? 1 : 0
z: 3
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font {
pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : defaultFont
}
text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue
elide: Text.ElideRight
color: (root.effectiveActiveWorkspaceId == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Rectangle { // Dot instead of ws number
id: wsDot
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|| root.showNumbers
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
) ? 0 : 1
visible: opacity > 0
anchors.centerIn: parent
width: workspaceButtonWidth * 0.18
height: width
radius: width / 2
color: (root.effectiveActiveWorkspaceId == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Item { // Main app icon
anchors.centerIn: parent
width: workspaceButtonWidth
height: workspaceButtonWidth
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
(workspaceButtonBackground.biggestWindow && !root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
visible: opacity > 0
IconImage {
id: mainAppIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
source: workspaceButtonBackground.mainAppIconSource
implicitSize: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitSize {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Loader {
active: Config.options.bar.workspaces.monochromeIcons
anchors.fill: mainAppIcon
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: mainAppIcon
desaturation: 0.8
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(wsDot.color, 0.9)
}
}
}
}
FadeLoader {
shown: !wsNum.showingNumbers
anchors.centerIn: parent
Circle {
anchors.centerIn: parent
diameter: root.workspaceButtonWidth * 0.18
color: wsNum.contentColor
}
}
FadeLoader {
shown: wsNum.showingNumbers
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: Config.options?.bar.workspaces.numberMap[wsNum.wsId - 1] || wsNum.wsId
}
}
}
component TrailingIndicator: Item {
id: trailingIndicator
anchors.fill: parent
required property int index
property alias indicatorRectangle: indicatorRect
property alias color: indicatorRect.color
property var indexPair: AnimatedTabIndexPair {
id: idxPair
index: trailingIndicator.index
}
StyledRectangle {
id: indicatorRect
anchors {
verticalCenter: root.vertical ? undefined : parent.verticalCenter
horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
}
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
contentLayer: StyledRectangle.ContentLayer.Group
radius: indicatorThickness / 2
color: Appearance.colors.colPrimary
x: root.vertical ? null : indicatorPosition
y: root.vertical ? indicatorPosition : null
implicitWidth: root.vertical ? indicatorThickness : indicatorLength
implicitHeight: root.vertical ? indicatorLength : indicatorThickness
}
}
}