import QtQuick import QtQuick.Layouts import QtQuick.Effects import Quickshell import Quickshell.Widgets import qs.config import qs.modules.components import qs.modules.functions import qs.services Item { id: workspaceContainer property string displayName: screen?.name ?? "" property int numWorkspaces: ConfigResolver.bar(displayName).modules.workspaces.workspaceIndicators property var workspaceOccupied: [] property var occupiedRanges: [] function japaneseNumber(num) { var kanjiMap = { "0": "零", "1": "一", "2": "二", "3": "三", "4": "四", "5": "五", "6": "六", "7": "七", "8": "八", "9": "九", "10": "十" }; return kanjiMap[num] !== undefined ? kanjiMap[num] : "Number out of range"; } function updateWorkspaceOccupied() { const offset = 1; workspaceOccupied = Array.from({ "length": numWorkspaces }, (_, i) => { return Compositor.isWorkspaceOccupied(i + 1); }); const ranges = []; let start = -1; for (let i = 0; i < workspaceOccupied.length; i++) { if (workspaceOccupied[i]) { if (start === -1) start = i; } else if (start !== -1) { ranges.push({ "start": start, "end": i - 1 }); start = -1; } } if (start !== -1) ranges.push({ "start": start, "end": workspaceOccupied.length - 1 }); occupiedRanges = ranges; } visible: ConfigResolver.bar(displayName).modules.workspaces.enabled implicitWidth: bg.implicitWidth implicitHeight: ConfigResolver.bar(displayName).modules.height Component.onCompleted: updateWorkspaceOccupied() Connections { function onStateChanged() { updateWorkspaceOccupied(); } target: Compositor } Rectangle { id: bg color: Appearance.m3colors.m3paddingContainer radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor implicitWidth: workspaceRow.implicitWidth + Metrics.margin("large") - 8 implicitHeight: ConfigResolver.bar(displayName).modules.height // occupied background highlight Item { id: occupiedStretchLayer anchors.centerIn: workspaceRow width: workspaceRow.width height: 26 z: 0 visible: Compositor.require("hyprland") // Hyprland only Repeater { model: occupiedRanges Rectangle { height: 26 radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor color: ColorUtils.mix(Appearance.m3colors.m3tertiary, Appearance.m3colors.m3surfaceContainerLowest) opacity: 0.8 x: modelData.start * (26 + workspaceRow.spacing) width: (modelData.end - modelData.start + 1) * 26 + (modelData.end - modelData.start) * workspaceRow.spacing } } } // workspace highlight Rectangle { id: highlight property int offset: Compositor.require("hyprland") ? 1 : 0 property int index: Math.max(0, Compositor.focusedWorkspaceId - 1 - offset) property real itemWidth: 26 property real spacing: workspaceRow.spacing property int highlightIndex: { if (!Compositor.focusedWorkspaceId) return 0; if (Compositor.require("hyprland")) return Compositor.focusedWorkspaceId - 1; // Hyprland starts at 2 internally return Compositor.focusedWorkspaceId - 2; // Niri or default } property real targetX: Math.min(highlightIndex, numWorkspaces - 1) * (itemWidth + spacing) + 7.3 property real animatedX1: targetX property real animatedX2: targetX x: Math.min(animatedX1, animatedX2) anchors.verticalCenter: parent.verticalCenter width: Math.abs(animatedX2 - animatedX1) + itemWidth - 1 height: 24 radius: ConfigResolver.bar(displayName).modules.radius * Config.runtime.appearance.rounding.factor color: Appearance.m3colors.m3tertiary onTargetXChanged: { animatedX1 = targetX; animatedX2 = targetX; } Behavior on animatedX1 { enabled: Config.runtime.appearance.animations.enabled NumberAnimation { duration: Metrics.chronoDuration(400) easing.type: Easing.OutSine } } Behavior on animatedX2 { enabled: Config.runtime.appearance.animations.enabled NumberAnimation { duration: Metrics.chronoDuration(133) easing.type: Easing.OutSine } } } RowLayout { id: workspaceRow anchors.centerIn: parent spacing: Metrics.spacing(10) Repeater { model: numWorkspaces Item { property int wsIndex: index + 1 property bool occupied: Compositor.isWorkspaceOccupied(wsIndex) property bool focused: wsIndex === Compositor.focusedWorkspaceId width: 26 height: 26 // Icon container — only used on Hyprland ClippingRectangle { id: iconContainer anchors.centerIn: parent width: 20 height: 20 color: "transparent" radius: Appearance.rounding.small clip: true IconImage { id: appIcon anchors.fill: parent visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && occupied rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0 source: { const win = Compositor.focusedWindowForWorkspace(wsIndex); return win ? AppRegistry.iconForClass(win.class) : ""; } layer.enabled: true layer.effect: MultiEffect { saturation: (Config.runtime.appearance.tintIcons || (Config.runtime.appearance.colors.matugenScheme === "scheme-monochrome" && Config.runtime.appearance.colors.autogenerated) || Config.runtime.appearance.colors.scheme.toLowerCase() === "monochrome") ? -1.0 : 1.0 } } } // Kanji mode — only if not Hyprland StyledText { anchors.centerIn: parent visible: ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons text: japaneseNumber(index + 1) rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0 } // Numbers mode — only if not Hyprland StyledText { anchors.centerIn: parent visible: !ConfigResolver.bar(displayName).modules.workspaces.showJapaneseNumbers && !ConfigResolver.bar(displayName).modules.workspaces.showAppIcons text: index + 1 rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0 } // Symbols for unoccupied workspaces — only for Hyprland icons MaterialSymbol { property string displayText: Config.runtime.appearance.rounding.factor === 0 ? "crop_square" : "fiber_manual_record" anchors.centerIn: parent visible: Compositor.require("hyprland") && ConfigResolver.bar(displayName).modules.workspaces.showAppIcons && !occupied text: displayText rotation: (ConfigResolver.bar(displayName).position === "left" || ConfigResolver.bar(displayName).position === "right") ? 270 : 0 font.pixelSize: Metrics.iconSize(10) fill: 1 } MouseArea { anchors.fill: parent onClicked: Compositor.changeWorkspace(wsIndex) } } } } } }