From 5d94c0a7d44a2255b81815a52a7056a94a39842d Mon Sep 17 00:00:00 2001 From: nippy Date: Sat, 18 Apr 2026 13:49:56 +0200 Subject: update Raveos themes --- .../Modules/DankBar/Widgets/WorkspaceSwitcher.qml | 1981 ++++++++++++++++++++ 1 file changed, 1981 insertions(+) create mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml') diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml new file mode 100644 index 0000000..3721e35 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml @@ -0,0 +1,1981 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import Quickshell.Hyprland +import Quickshell.I3 +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property bool isVertical: axis?.isVertical ?? false + property var axis: null + property string screenName: "" + property real widgetHeight: 30 + property real barThickness: 48 + property var barConfig: null + property var blurBarWindow: null + property var hyprlandOverviewLoader: null + property var parentScreen: null + + readonly property real _leftMargin: { + if (isVertical) + return 0; + root.x; + if (!root.parent) + return 0; + const gap = root.mapToItem(null, 0, 0).x; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + readonly property real _rightMargin: { + if (isVertical) + return 0; + root.x; + root.width; + if (!root.parent || !blurBarWindow) + return 0; + const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + readonly property real _topMargin: { + if (!isVertical) + return 0; + root.y; + if (!root.parent) + return 0; + const gap = root.mapToItem(null, 0, 0).y; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + readonly property real _bottomMargin: { + if (!isVertical) + return 0; + root.y; + root.height; + if (!root.parent || !blurBarWindow) + return 0; + const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y; + return (gap > 0 && gap < 30) ? gap + 5 : 0; + } + + property int _desktopEntriesUpdateTrigger: 0 + readonly property var sortedToplevels: { + return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + } + + readonly property string effectiveScreenName: { + if (!SettingsData.workspaceFollowFocus) + return root.screenName; + + switch (CompositorService.compositor) { + case "niri": + return NiriService.currentOutput || root.screenName; + case "hyprland": + return Hyprland.focusedWorkspace?.monitor?.name || root.screenName; + case "dwl": + return DwlService.activeOutput || root.screenName; + case "sway": + case "scroll": + case "miracle": + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + return focusedWs?.monitor?.name || root.screenName; + default: + return root.screenName; + } + } + + readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable) + + Connections { + target: DesktopEntries + function onApplicationsChanged() { + _desktopEntriesUpdateTrigger++; + } + } + + property var currentWorkspace: { + if (useExtWorkspace) + return getExtWorkspaceActiveWorkspace(); + + switch (CompositorService.compositor) { + case "niri": + return getNiriActiveWorkspace(); + case "hyprland": + return getHyprlandActiveWorkspace(); + case "dwl": + const activeTags = getDwlActiveTags(); + return activeTags.length > 0 ? activeTags[0] : -1; + case "sway": + case "scroll": + case "miracle": + return getSwayActiveWorkspace(); + default: + return 1; + } + } + property var dwlActiveTags: { + if (CompositorService.isDwl) { + return getDwlActiveTags(); + } + return []; + } + property var workspaceList: { + if (useExtWorkspace) { + const baseList = getExtWorkspaceWorkspaces(); + return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList; + } + + let baseList; + switch (CompositorService.compositor) { + case "niri": + baseList = getNiriWorkspaces(); + break; + case "hyprland": + baseList = getHyprlandWorkspaces(); + break; + case "dwl": + baseList = getDwlTags(); + break; + case "sway": + case "scroll": + case "miracle": + baseList = getSwayWorkspaces(); + break; + default: + return [1]; + } + return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList; + } + + function getSwayWorkspaces() { + const workspaces = I3.workspaces?.values || []; + if (workspaces.length === 0) + return [ + { + "num": 1 + } + ]; + + function mapWorkspace(ws) { + return { + "num": ws.number, + "name": ws.name, + "focused": ws.focused, + "active": ws.active, + "urgent": ws.urgent, + "monitor": ws.monitor + }; + } + + if (!root.screenName || SettingsData.workspaceFollowFocus) { + return workspaces.slice().sort((a, b) => a.num - b.num).map(mapWorkspace); + } + + const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName); + return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num).map(mapWorkspace) : [ + { + "num": 1 + } + ]; + } + + function getSwayActiveWorkspace() { + if (!root.screenName || SettingsData.workspaceFollowFocus) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + return focusedWs ? focusedWs.num : 1; + } + + const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true); + return focusedWs ? focusedWs.num : 1; + } + + function getHyprlandWorkspaces() { + const workspaces = Hyprland.workspaces?.values || []; + if (workspaces.length === 0) { + return [ + { + id: 1, + name: "1" + } + ]; + } + + let filtered = workspaces.filter(ws => ws.id > -1); + if (filtered.length === 0) { + return [ + { + id: 1, + name: "1" + } + ]; + } + + if (!root.screenName || SettingsData.workspaceFollowFocus) { + filtered = filtered.slice().sort((a, b) => a.id - b.id); + } else { + const monitorWorkspaces = filtered.filter(ws => ws.monitor?.name === root.screenName); + filtered = monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [ + { + id: 1, + name: "1" + } + ]; + } + + if (!SettingsData.showOccupiedWorkspacesOnly) { + return filtered; + } + + const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); + const activeWsId = root.currentWorkspace; + return filtered.filter(ws => { + if (ws.id === activeWsId) + return true; + return hyprlandToplevels.some(tl => tl.workspace?.id === ws.id); + }); + } + + function getHyprlandActiveWorkspace() { + if (!root.screenName || SettingsData.workspaceFollowFocus) { + return Hyprland.focusedWorkspace?.id || 1; + } + + const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName); + return monitor?.activeWorkspace?.id || 1; + } + + function getWorkspaceIcons(ws) { + _desktopEntriesUpdateTrigger; + if (!SettingsData.showWorkspaceApps || !ws) { + return []; + } + + let targetWorkspaceId; + if (CompositorService.isNiri) { + if (!ws || typeof ws !== "object") { + const wsNumber = typeof ws === "number" ? ws : -1; + if (wsNumber <= 0) { + return []; + } + const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.effectiveScreenName); + if (!workspace) { + return []; + } + targetWorkspaceId = workspace.id; + } else { + if (ws.id === undefined || ws.id === -1 || ws.idx === -1) { + return []; + } + targetWorkspaceId = ws.id; + } + } else if (CompositorService.isHyprland) { + targetWorkspaceId = ws.id !== undefined ? ws.id : ws; + } else if (CompositorService.isDwl) { + if (typeof ws !== "object" || ws.tag === undefined) { + return []; + } + targetWorkspaceId = ws.tag; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + targetWorkspaceId = ws.num !== undefined ? ws.num : ws; + } else { + return []; + } + + const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels; + + const byApp = {}; + let isActiveWs = false; + if (CompositorService.isNiri) { + isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active); + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); + isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false; + } else if (CompositorService.isDwl) { + const output = DwlService.getOutputState(root.effectiveScreenName); + if (output && output.tags) { + const tag = output.tags.find(t => t.tag === targetWorkspaceId); + isActiveWs = tag ? (tag.state === 1) : false; + } + } else { + isActiveWs = targetWorkspaceId === root.currentWorkspace; + } + + wins.forEach((w, i) => { + if (!w) { + return; + } + + let winWs = null; + if (CompositorService.isNiri) { + winWs = w.workspace_id; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + winWs = w.workspace?.num; + } else { + const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); + const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w); + winWs = hyprToplevel?.workspace?.id; + } + + if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) { + return; + } + + const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown"); + const moddedId = Paths.moddedAppId(keyBase); + const key = isActiveWs || !SettingsData.groupWorkspaceApps ? `${moddedId}_${i}` : moddedId; + + if (!byApp[key]) { + const isQuickshell = keyBase === "org.quickshell" || keyBase === "com.danklinux.dms"; + const isSteamApp = Paths.isSteamApp(moddedId); + const desktopEntry = DesktopEntries.heuristicLookup(moddedId); + const icon = Paths.getAppIcon(moddedId, desktopEntry); + const appName = Paths.getAppName(moddedId, desktopEntry); + byApp[key] = { + "type": "icon", + "icon": icon, + "isQuickshell": isQuickshell, + "isSteamApp": isSteamApp, + "active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)), + "count": 1, + "windowId": w.address || w.id, + "fallbackText": appName || "" + }; + } else { + byApp[key].count++; + if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) { + byApp[key].active = true; + } + } + }); + + return Object.values(byApp); + } + + function padWorkspaces(list) { + const padded = list.slice(); + let placeholder; + if (useExtWorkspace) { + placeholder = { + "id": "", + "name": "", + "active": false, + "hidden": true + }; + } else if (CompositorService.isNiri) { + placeholder = { + "id": -1, + "idx": -1, + "name": "" + }; + } else if (CompositorService.isHyprland) { + placeholder = { + "id": -1, + "name": "" + }; + } else if (CompositorService.isDwl) { + placeholder = { + "tag": -1 + }; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + placeholder = { + "num": -1 + }; + } else { + placeholder = -1; + } + while (padded.length < 3) { + padded.push(placeholder); + } + return padded; + } + + function getNiriWorkspaces() { + if (NiriService.allWorkspaces.length === 0) { + return [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; + } + + const fallbackWorkspaces = [ + { + "id": 1, + "idx": 0, + "name": "" + }, + { + "id": 2, + "idx": 1, + "name": "" + } + ]; + + let workspaces; + if (!root.screenName || SettingsData.workspaceFollowFocus) { + const currentWorkspaces = NiriService.getCurrentOutputWorkspaces(); + workspaces = currentWorkspaces.length > 0 ? currentWorkspaces : fallbackWorkspaces; + } else { + const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName); + workspaces = displayWorkspaces.length > 0 ? displayWorkspaces : fallbackWorkspaces; + } + + workspaces = workspaces.slice().sort((a, b) => a.idx - b.idx); + + if (!SettingsData.showOccupiedWorkspacesOnly) { + return workspaces; + } + + return workspaces.filter(ws => { + if (ws.is_active) + return true; + return NiriService.windows?.some(win => win.workspace_id === ws.id) ?? false; + }); + } + + function getNiriActiveWorkspace() { + if (NiriService.allWorkspaces.length === 0) { + return 1; + } + + if (!root.screenName || SettingsData.workspaceFollowFocus) { + return NiriService.getCurrentWorkspaceNumber(); + } + + const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active); + return activeWs ? activeWs.idx : 1; + } + + function getDwlTags() { + if (!DwlService.dwlAvailable) + return []; + + const targetScreen = root.effectiveScreenName; + const output = DwlService.getOutputState(targetScreen); + if (!output || !output.tags || output.tags.length === 0) + return []; + + if (SettingsData.dwlShowAllTags) { + return output.tags.map(tag => ({ + "tag": tag.tag, + "state": tag.state, + "clients": tag.clients, + "focused": tag.focused + })); + } + + const visibleTagIndices = DwlService.getVisibleTags(targetScreen); + return visibleTagIndices.map(tagIndex => { + const tagData = output.tags.find(t => t.tag === tagIndex); + return { + "tag": tagIndex, + "state": tagData?.state ?? 0, + "clients": tagData?.clients ?? 0, + "focused": tagData?.focused ?? false + }; + }); + } + + function getDwlActiveTags() { + if (!DwlService.dwlAvailable) + return []; + + return DwlService.getActiveTags(root.effectiveScreenName); + } + + function getExtWorkspaceWorkspaces() { + const groups = ExtWorkspaceService.groups; + if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) { + return [ + { + "id": "1", + "name": "1", + "active": false + } + ]; + } + + const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName)); + if (!group || !group.workspaces) { + return [ + { + "id": "1", + "name": "1", + "active": false + } + ]; + } + + let visible = group.workspaces.filter(ws => !ws.hidden); + + const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0); + if (hasValidCoordinates) { + visible = visible.sort((a, b) => { + const coordsA = a.coordinates || [0, 0]; + const coordsB = b.coordinates || [0, 0]; + if (coordsA[0] !== coordsB[0]) + return coordsA[0] - coordsB[0]; + return coordsA[1] - coordsB[1]; + }); + } + + visible = visible.map(ws => ({ + id: ws.id, + name: ws.name, + coordinates: ws.coordinates, + state: ws.state, + active: ws.active, + urgent: ws.urgent, + hidden: ws.hidden, + groupID: group.id + })); + + return visible.length > 0 ? visible : [ + { + "id": "1", + "name": "1", + "active": false + } + ]; + } + + function getExtWorkspaceActiveWorkspace() { + if (!ExtWorkspaceService.extWorkspaceAvailable) { + return 1; + } + + const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName); + return activeWs ? (activeWs.id || activeWs.name || "1") : "1"; + } + + readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1 + readonly property real padding: (root.barConfig?.removeWidgetPadding ?? false) ? 0 : Theme.snap((root.barConfig?.widgetPadding ?? 12) * (widgetHeight / 30), dpr) + readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2) + readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight + readonly property real appIconSize: Theme.barIconSize(barThickness, -6 + SettingsData.workspaceAppIconSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + + function getRealWorkspaces() { + return root.workspaceList.filter(ws => { + if (useExtWorkspace) + return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden; + if (CompositorService.isNiri) + return ws && ws.idx !== -1; + if (CompositorService.isHyprland) + return ws && ws.id !== -1; + if (CompositorService.isDwl) + return ws && ws.tag !== -1; + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return ws && ws.num !== -1; + return ws !== -1; + }); + } + + function switchToWorkspaceByModelData(data) { + if (!data) + return; + + if (root.useExtWorkspace && (data.id || data.name)) { + ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || ""); + return; + } + + switch (CompositorService.compositor) { + case "niri": + if (data.idx !== undefined) + NiriService.switchToWorkspace(data.idx); + break; + case "hyprland": + if (data.id) + Hyprland.dispatch(`workspace ${data.id}`); + break; + case "dwl": + if (data.tag !== undefined) + DwlService.switchToTag(root.screenName, data.tag); + break; + case "sway": + case "scroll": + case "miracle": + if (data.num) + try { + I3.dispatch(`workspace number ${data.num}`); + } catch (_) {} + break; + } + } + + function findClosestWorkspaceIndex(localX, localY) { + if (workspaceRepeater.count === 0) + return -1; + + let closestIdx = -1; + let closestDist = Infinity; + + for (let i = 0; i < workspaceRepeater.count; i++) { + const item = workspaceRepeater.itemAt(i); + if (!item) + continue; + const center = item.mapToItem(root, item.width / 2, item.height / 2); + const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x); + if (dist < closestDist) { + closestDist = dist; + closestIdx = i; + } + } + return closestIdx; + } + + function switchWorkspace(direction) { + if (useExtWorkspace) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + const nextWorkspace = realWorkspaces[nextIndex]; + ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || ""); + } else if (CompositorService.isNiri) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws && ws.idx === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + const nextWorkspace = realWorkspaces[nextIndex]; + if (!nextWorkspace || nextWorkspace.idx === undefined) { + return; + } + NiriService.switchToWorkspace(nextWorkspace.idx); + } else if (CompositorService.isHyprland) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`); + } else if (CompositorService.isDwl) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag); + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + const realWorkspaces = getRealWorkspaces(); + if (realWorkspaces.length < 2) { + return; + } + + const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace); + const validIndex = currentIndex === -1 ? 0 : currentIndex; + const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); + + if (nextIndex === validIndex) { + return; + } + + try { + I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`); + } catch (_) {} + } + } + + function getWorkspaceIndexFallback(modelData, index) { + if (root.useExtWorkspace) + return index + 1; + if (CompositorService.isNiri) + return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : ""; + if (CompositorService.isHyprland) + return modelData?.id || ""; + if (CompositorService.isDwl) + return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return modelData?.num || ""; + return modelData - 1; + } + + function getWorkspaceIndex(modelData, index) { + let isPlaceholder; + if (root.useExtWorkspace) { + isPlaceholder = modelData?.hidden === true; + } else if (CompositorService.isNiri) { + isPlaceholder = modelData?.idx === -1; + } else if (CompositorService.isHyprland) { + isPlaceholder = modelData?.id === -1; + } else if (CompositorService.isDwl) { + isPlaceholder = modelData?.tag === -1; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + isPlaceholder = modelData?.num === -1; + } else { + isPlaceholder = modelData === -1; + } + + if (isPlaceholder) + return index + 1; + + let workspaceName = ""; + if (SettingsData.showWorkspaceName) { + workspaceName = modelData?.name ?? ""; + + if (workspaceName && workspaceName !== "") { + if (root.isVertical) { + workspaceName = workspaceName.charAt(0); + } + } else { + workspaceName = ""; + } + } + + if (workspaceName) { + if (SettingsData.showWorkspaceIndex) { + const indexLabel = getWorkspaceIndexFallback(modelData, index); + return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName; + } + return workspaceName; + } + + return getWorkspaceIndexFallback(modelData, index); + } + + readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle + readonly property bool hasWorkspaces: getRealWorkspaces().length > 0 + readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) + + width: shouldShow ? (isVertical ? barThickness : visualWidth) : 0 + height: shouldShow ? (isVertical ? visualHeight : barThickness) : 0 + visible: shouldShow + + Item { + id: visualBackground + width: root.visualWidth + height: root.visualHeight + anchors.centerIn: parent + + Rectangle { + id: outline + anchors.centerIn: parent + width: { + const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return parent.width + borderWidth * 2; + } + height: { + const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0; + return parent.height + borderWidth * 2; + } + radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius + color: "transparent" + border.width: { + if (barConfig?.widgetOutlineEnabled ?? false) { + return barConfig?.widgetOutlineThickness ?? 1; + } + return 0; + } + border.color: { + if (!(barConfig?.widgetOutlineEnabled ?? false)) { + return "transparent"; + } + const colorOption = barConfig?.widgetOutlineColor || "primary"; + const opacity = barConfig?.widgetOutlineOpacity ?? 1.0; + switch (colorOption) { + case "surfaceText": + return Theme.withAlpha(Theme.surfaceText, opacity); + case "secondary": + return Theme.withAlpha(Theme.secondary, opacity); + case "primary": + return Theme.withAlpha(Theme.primary, opacity); + default: + return Theme.withAlpha(Theme.primary, opacity); + } + } + } + + Rectangle { + id: background + anchors.fill: parent + radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius + color: { + if ((barConfig?.noBackground ?? false)) + return "transparent"; + const baseColor = Theme.widgetBaseBackgroundColor; + const transparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0; + if (Theme.widgetBackgroundHasAlpha) { + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency); + } + return Theme.withAlpha(baseColor, transparency); + } + } + } + + MouseArea { + id: edgeMouseArea + z: -1 + x: -root._leftMargin + y: -root._topMargin + width: root.width + root._leftMargin + root._rightMargin + height: root.height + root._topMargin + root._bottomMargin + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + property real touchpadAccumulator: 0 + property real mouseAccumulator: 0 + property bool scrollInProgress: false + + Timer { + id: scrollCooldown + interval: 100 + onTriggered: parent.scrollInProgress = false + } + + onClicked: mouse => { + const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y); + switch (mouse.button) { + case Qt.RightButton: + if (CompositorService.isNiri) { + NiriService.toggleOverview(); + } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { + root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; + } + break; + case Qt.LeftButton: + const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y); + if (idx >= 0) + root.switchToWorkspaceByModelData(root.workspaceList[idx]); + break; + } + } + + onWheel: wheel => { + if (Math.abs(wheel.angleDelta.x) > Math.abs(wheel.angleDelta.y)) { + wheel.accepted = false; + return; + } + + if (scrollInProgress) + return; + + const delta = wheel.angleDelta.y; + const isTouchpad = wheel.pixelDelta && wheel.pixelDelta.y !== 0; + const reverse = SettingsData.reverseScrolling ? -1 : 1; + + if (isTouchpad) { + touchpadAccumulator += delta; + if (Math.abs(touchpadAccumulator) < 500) + return; + const direction = touchpadAccumulator * reverse < 0 ? 1 : -1; + root.switchWorkspace(direction); + scrollInProgress = true; + scrollCooldown.restart(); + touchpadAccumulator = 0; + return; + } + + mouseAccumulator += delta; + if (Math.abs(mouseAccumulator) < 120) + return; + const direction = mouseAccumulator * reverse < 0 ? 1 : -1; + root.switchWorkspace(direction); + scrollInProgress = true; + scrollCooldown.restart(); + mouseAccumulator = 0; + } + } + + property int dragSourceIndex: -1 + property int dragTargetIndex: -1 + property bool suppressShiftAnimation: false + + onWorkspaceListChanged: { + if (dragSourceIndex >= 0) { + dragSourceIndex = -1; + dragTargetIndex = -1; + suppressShiftAnimation = false; + } + } + + Flow { + id: workspaceRow + + x: isVertical ? visualBackground.x : (parent.width - implicitWidth) / 2 + y: isVertical ? (parent.height - implicitHeight) / 2 : visualBackground.y + spacing: Theme.spacingS + flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + id: workspaceRepeater + model: ScriptModel { + values: root.workspaceList + } + + Item { + id: delegateRoot + + property bool isDropTarget: root.dragTargetIndex === index + + z: dragHandler.dragging ? 1000 : 1 + + property real shiftOffset: { + if (root.dragSourceIndex < 0 || index === root.dragSourceIndex) + return 0; + const dragIdx = root.dragSourceIndex; + const dropIdx = root.dragTargetIndex; + if (dropIdx < 0) + return 0; + const shiftAmount = delegateRoot.width + Theme.spacingS; + if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx) + return -shiftAmount; + if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx) + return shiftAmount; + return 0; + } + + transform: Translate { + x: root.isVertical ? 0 : delegateRoot.shiftOffset + y: root.isVertical ? delegateRoot.shiftOffset : 0 + Behavior on x { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + Behavior on y { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + property bool isActive: { + if (root.useExtWorkspace) + return (modelData?.id || modelData?.name) === root.currentWorkspace; + if (CompositorService.isNiri) + return !!(modelData && modelData.idx === root.currentWorkspace); + if (CompositorService.isHyprland) + return !!(modelData && modelData.id === root.currentWorkspace); + if (CompositorService.isDwl) + return !!(modelData && root.dwlActiveTags.includes(modelData.tag)); + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return !!(modelData && modelData.num === root.currentWorkspace); + return modelData === root.currentWorkspace; + } + property bool isOccupied: { + if (CompositorService.isHyprland) + return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id); + if (CompositorService.isDwl) + return modelData.clients > 0; + if (CompositorService.isNiri) { + const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName); + return workspace ? (NiriService.windows?.some(win => win.workspace_id === workspace.id) ?? false) : false; + } + return false; + } + property bool isPlaceholder: { + if (root.useExtWorkspace) + return !!(modelData && modelData.hidden); + if (CompositorService.isNiri) + return !!(modelData && modelData.idx === -1); + if (CompositorService.isHyprland) + return !!(modelData && modelData.id === -1); + if (CompositorService.isDwl) + return !!(modelData && modelData.tag === -1); + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return !!(modelData && modelData.num === -1); + return modelData === -1; + } + property bool isHovered: mouseArea.containsMouse + + property var loadedWorkspaceData: null + property bool loadedIsUrgent: false + property bool isUrgent: { + if (root.useExtWorkspace) + return modelData?.urgent ?? false; + if (CompositorService.isHyprland) + return modelData?.urgent ?? false; + if (CompositorService.isNiri) + return loadedIsUrgent; + if (CompositorService.isDwl) + return modelData?.state === 2; + if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + return loadedIsUrgent; + return false; + } + readonly property var loadedIconData: { + if (isPlaceholder) + return null; + const name = modelData?.name; + if (!name) + return null; + return SettingsData.getWorkspaceNameIcon(name); + } + readonly property bool loadedHasIcon: loadedIconData !== null + property var loadedIcons: [] + + readonly property int stableIconCount: { + if (!SettingsData.showWorkspaceApps || isPlaceholder) + return 0; + + let targetWorkspaceId; + if (root.useExtWorkspace) { + targetWorkspaceId = modelData?.id || modelData?.name; + } else if (CompositorService.isNiri) { + targetWorkspaceId = modelData?.id; + } else if (CompositorService.isHyprland) { + targetWorkspaceId = modelData?.id; + } else if (CompositorService.isDwl) { + targetWorkspaceId = modelData?.tag; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + targetWorkspaceId = modelData?.num; + } + if (targetWorkspaceId === undefined || targetWorkspaceId === null) + return 0; + + const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels; + const seen = {}; + let groupedCount = 0; + let totalCount = 0; + + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + if (!w) + continue; + + let winWs = null; + if (CompositorService.isNiri) { + winWs = w.workspace_id; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + winWs = w.workspace?.num; + } else if (CompositorService.isHyprland) { + const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); + const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w); + winWs = hyprToplevel?.workspace?.id; + } + + if (winWs !== targetWorkspaceId) + continue; + totalCount++; + + const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown"; + if (!seen[appKey]) { + seen[appKey] = true; + groupedCount++; + } + } + + return (SettingsData.groupWorkspaceApps && !isActive) ? groupedCount : totalCount; + } + + readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) + readonly property real baseHeight: root.isVertical ? (isActive ? Math.max(root.widgetHeight * 1.05, root.appIconSize * 1.6) : Math.max(root.widgetHeight * 0.7, root.appIconSize * 1.2)) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) + readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== "" + readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0 + readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0 + + readonly property real iconsExtraWidth: { + if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) { + const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons); + return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0); + } + return 0; + } + readonly property real iconsExtraHeight: { + if (root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) { + const numIcons = Math.min(stableIconCount, SettingsData.maxWorkspaceIcons); + return numIcons * root.appIconSize + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0); + } + return 0; + } + + readonly property real visualWidth: { + if (contentImplicitWidth <= 0) + return baseWidth + iconsExtraWidth; + const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS; + return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding); + } + readonly property real visualHeight: { + if (contentImplicitHeight <= 0) + return baseHeight + iconsExtraHeight; + const padding = root.isVertical ? Theme.spacingS : Theme.spacingXS; + return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding); + } + + readonly property color unfocusedColor: { + switch (SettingsData.workspaceUnfocusedColorMode) { + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + case "sch": + return Theme.surfaceContainerHigh; + default: + return Theme.surfaceTextAlpha; + } + } + + readonly property color activeColor: { + switch (SettingsData.workspaceColorMode) { + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + case "sch": + return Theme.surfaceContainerHigh; + case "none": + return unfocusedColor; + default: + return Theme.primary; + } + } + + readonly property color occupiedColor: { + switch (SettingsData.workspaceOccupiedColorMode) { + case "sec": + return Theme.secondary; + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + case "sch": + return Theme.surfaceContainerHigh; + case "schh": + return Theme.surfaceContainerHighest; + default: + return unfocusedColor; + } + } + + readonly property color urgentColor: { + switch (SettingsData.workspaceUrgentColorMode) { + case "primary": + return Theme.primary; + case "secondary": + return Theme.secondary; + case "s": + return Theme.surface; + case "sc": + return Theme.surfaceContainer; + default: + return Theme.error; + } + } + + readonly property color focusedBorderColor: { + switch (SettingsData.workspaceFocusedBorderColor) { + case "surfaceText": + return Theme.surfaceText; + case "secondary": + return Theme.secondary; + default: + return Theme.primary; + } + } + + function getContrastingIconColor(bgColor) { + const luminance = 0.299 * bgColor.r + 0.587 * bgColor.g + 0.114 * bgColor.b; + return luminance > 0.4 ? Qt.rgba(0.15, 0.15, 0.15, 1) : Qt.rgba(0.8, 0.8, 0.8, 1); + } + + readonly property color quickshellIconActiveColor: getContrastingIconColor(activeColor) + readonly property color quickshellIconInactiveColor: getContrastingIconColor(unfocusedColor) + + Item { + id: dragHandler + anchors.fill: parent + property bool dragging: false + property point dragStartPos: Qt.point(0, 0) + property real dragAxisOffset: 0 + + Connections { + target: root + function onWorkspaceListChanged() { + if (dragHandler.dragging) { + dragHandler.dragging = false; + dragHandler.dragAxisOffset = 0; + mouseArea.mousePressed = false; + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: !isPlaceholder + cursorShape: isPlaceholder ? Qt.ArrowCursor : (dragHandler.dragging ? Qt.ClosedHandCursor : Qt.PointingHandCursor) + enabled: !isPlaceholder + acceptedButtons: Qt.LeftButton | Qt.RightButton + + property bool mousePressed: false + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && CompositorService.isNiri && SettingsData.workspaceDragReorder && !isPlaceholder) { + mousePressed = true; + dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y); + } + } + + onPositionChanged: mouse => { + if (!mousePressed || !CompositorService.isNiri || !SettingsData.workspaceDragReorder || isPlaceholder) + return; + + if (!dragHandler.dragging) { + const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x); + if (distance > 5) { + dragHandler.dragging = true; + root.dragSourceIndex = index; + root.dragTargetIndex = index; + } + } + + if (!dragHandler.dragging) + return; + + const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x); + + const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS; + const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize; + const maxOffsetNegative = -index * itemSize; + const axisOffset = Math.max(maxOffsetNegative, Math.min(maxOffsetPositive, rawAxisOffset)); + dragHandler.dragAxisOffset = axisOffset; + + const slotOffset = Math.round(axisOffset / itemSize); + const newTargetIndex = Math.max(0, Math.min(root.workspaceList.length - 1, index + slotOffset)); + + if (newTargetIndex !== root.dragTargetIndex) { + root.dragTargetIndex = newTargetIndex; + } + } + + onReleased: mouse => { + const wasDragging = dragHandler.dragging; + const didReorder = wasDragging && root.dragTargetIndex >= 0 && root.dragTargetIndex !== root.dragSourceIndex; + + if (didReorder) { + const sourceWs = root.workspaceList[root.dragSourceIndex]; + const targetWs = root.workspaceList[root.dragTargetIndex]; + + if (sourceWs && targetWs && sourceWs.idx !== undefined && targetWs.idx !== undefined) { + root.suppressShiftAnimation = true; + NiriService.moveWorkspaceToIndex(sourceWs.idx, targetWs.idx); + Qt.callLater(() => root.suppressShiftAnimation = false); + } + } + + mousePressed = false; + dragHandler.dragging = false; + dragHandler.dragAxisOffset = 0; + root.dragSourceIndex = -1; + root.dragTargetIndex = -1; + + if (wasDragging || isPlaceholder) + return; + + if (mouse.button === Qt.LeftButton) { + if (root.useExtWorkspace && (modelData?.id || modelData?.name)) { + ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || ""); + } else if (CompositorService.isNiri) { + if (modelData && modelData.idx !== undefined) { + NiriService.switchToWorkspace(modelData.idx); + } + } else if (CompositorService.isHyprland && modelData?.id) { + Hyprland.dispatch(`workspace ${modelData.id}`); + } else if (CompositorService.isDwl && modelData?.tag !== undefined) { + DwlService.switchToTag(root.screenName, modelData.tag); + } else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) { + try { + I3.dispatch(`workspace number ${modelData.num}`); + } catch (_) {} + } + } else if (mouse.button === Qt.RightButton) { + if (CompositorService.isNiri) { + NiriService.toggleOverview(); + } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { + root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; + } else if (CompositorService.isDwl && modelData?.tag !== undefined) { + DwlService.toggleTag(root.screenName, modelData.tag); + } + } + } + } + + Timer { + id: dataUpdateTimer + interval: 50 + onTriggered: { + if (isPlaceholder) { + delegateRoot.loadedWorkspaceData = null; + delegateRoot.loadedIcons = []; + delegateRoot.loadedIsUrgent = false; + return; + } + + var wsData = null; + if (root.useExtWorkspace) { + wsData = modelData; + } else if (CompositorService.isNiri) { + wsData = modelData || null; + } else if (CompositorService.isHyprland) { + wsData = modelData; + } else if (CompositorService.isDwl) { + wsData = modelData; + } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + wsData = modelData; + } + delegateRoot.loadedWorkspaceData = wsData; + if (CompositorService.isNiri) { + const workspaceId = wsData?.id; + delegateRoot.loadedIsUrgent = workspaceId ? NiriService.windows.some(w => w.workspace_id === workspaceId && w.is_urgent) : false; + } else { + delegateRoot.loadedIsUrgent = wsData?.urgent ?? false; + } + + if (SettingsData.showWorkspaceApps) { + if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { + delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); + } else if (CompositorService.isNiri) { + delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData); + } else { + delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData)); + } + } else { + delegateRoot.loadedIcons = []; + } + } + } + + function updateAllData() { + dataUpdateTimer.restart(); + } + + width: root.isVertical ? root.widgetHeight : visualWidth + height: root.isVertical ? visualHeight : root.widgetHeight + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Rectangle { + id: focusedBorderRing + x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2 + y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2 + width: { + const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0; + return delegateRoot.visualWidth + borderWidth * 2; + } + height: { + const borderWidth = (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0; + return delegateRoot.visualHeight + borderWidth * 2; + } + radius: Theme.cornerRadius + color: "transparent" + border.width: (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? SettingsData.workspaceFocusedBorderThickness : 0 + border.color: (SettingsData.workspaceFocusedBorderEnabled && isActive && !isPlaceholder) ? focusedBorderColor : "transparent" + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + Rectangle { + id: visualContent + width: delegateRoot.visualWidth + height: delegateRoot.visualHeight + x: root.isVertical ? (root.widgetHeight - width) / 2 : (parent.width - width) / 2 + y: root.isVertical ? (parent.height - height) / 2 : (root.widgetHeight - height) / 2 + radius: Theme.cornerRadius + color: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, 0.7) : isOccupied ? occupiedColor : unfocusedColor + opacity: dragHandler.dragging ? 0.8 : 1.0 + + border.width: dragHandler.dragging ? 2 : (isUrgent ? 2 : (isDropTarget ? 2 : 0)) + border.color: dragHandler.dragging ? Theme.primary : (isUrgent ? urgentColor : (isDropTarget ? Theme.primary : "transparent")) + + transform: Translate { + x: root.isVertical ? 0 : (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) + y: root.isVertical ? (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) : 0 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.width { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on border.color { + ColorAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Loader { + id: appIconsLoader + anchors.fill: parent + active: SettingsData.showWorkspaceApps || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon + sourceComponent: Item { + id: contentRoot + readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0 + readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0 + + Loader { + id: contentRow + anchors.centerIn: parent + sourceComponent: root.isVertical ? columnLayout : rowLayout + } + + Component { + id: rowLayout + Row { + spacing: 4 + visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon + + Item { + visible: loadedHasIcon && loadedIconData?.type === "icon" + width: wsIcon.width + height: root.appIconSize + + DankIcon { + id: wsIcon + anchors.verticalCenter: parent.verticalCenter + name: loadedIconData?.value ?? "" + size: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + weight: (isActive && !isPlaceholder) ? 500 : 400 + } + } + + Item { + visible: loadedHasIcon && loadedIconData?.type === "text" + width: wsText.implicitWidth + height: root.appIconSize + + StyledText { + id: wsText + anchors.verticalCenter: parent.verticalCenter + text: loadedIconData?.value ?? "" + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + } + + Item { + visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName) + width: wsIndexText.implicitWidth + height: root.appIconSize + + StyledText { + id: wsIndexText + anchors.verticalCenter: parent.verticalCenter + text: loadedHasIcon ? (modelData?.name ?? "") : root.getWorkspaceIndex(modelData, index) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + } + + Repeater { + model: ScriptModel { + values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons) + } + delegate: Item { + width: root.appIconSize + height: root.appIconSize + readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active + readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected + readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary + readonly property real appOpacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + + IconImage { + id: rowAppIcon + anchors.fill: parent + source: modelData.icon || "" + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && !modelData.isSteamApp && rowAppIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + StyledText { + anchors.centerIn: parent + text: (modelData.fallbackText || "?").charAt(0).toUpperCase() + font.pixelSize: parent.width * 0.45 + color: appGlyphColor + font.weight: Font.Bold + } + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && modelData.isSteamApp && rowSteamIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + DankIcon { + anchors.centerIn: parent + size: parent.width * 0.7 + name: "sports_esports" + color: appGlyphColor + } + } + + IconImage { + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isQuickshell + layer.enabled: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) + } + } + + IconImage { + id: rowSteamIcon + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && modelData.icon + } + + DankIcon { + anchors.centerIn: parent + size: root.appIconSize + name: "sports_esports" + color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor + opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && !modelData.icon + } + + Rectangle { + anchors.fill: parent + visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive + color: "transparent" + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: focusedBorderColor + z: 1 + } + + MouseArea { + id: rowAppMouseArea + anchors.fill: parent + enabled: isActive + cursorShape: Qt.PointingHandCursor + onClicked: { + const winId = modelData.windowId; + if (!winId) + return; + if (CompositorService.isHyprland) { + Hyprland.dispatch(`focuswindow address:${winId}`); + } else if (CompositorService.isNiri) { + NiriService.focusWindow(winId); + } + } + } + + Rectangle { + visible: modelData.count > 1 && !isActive + width: root.appIconSize * 0.67 + height: root.appIconSize * 0.67 + radius: root.appIconSize * 0.33 + color: "black" + border.color: "white" + border.width: 1 + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 2 + + Text { + anchors.centerIn: parent + text: modelData.count + font.pixelSize: root.appIconSize * 0.44 + color: "white" + } + } + } + } + } + } + + Component { + id: columnLayout + Column { + spacing: 4 + visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName || loadedHasIcon + + DankIcon { + visible: loadedHasIcon && loadedIconData?.type === "icon" + anchors.horizontalCenter: parent.horizontalCenter + name: loadedIconData?.value ?? "" + size: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + weight: (isActive && !isPlaceholder) ? 500 : 400 + } + + StyledText { + visible: loadedHasIcon && loadedIconData?.type === "text" + anchors.horizontalCenter: parent.horizontalCenter + text: loadedIconData?.value ?? "" + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + + StyledText { + visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName) + anchors.horizontalCenter: parent.horizontalCenter + text: loadedHasIcon ? (root.isVertical ? (modelData?.name ?? "").charAt(0) : (modelData?.name ?? "")) : root.getWorkspaceIndex(modelData, index) + color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium + font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText) + font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal + } + + Repeater { + model: ScriptModel { + values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons) + } + delegate: Item { + width: root.appIconSize + height: root.appIconSize + readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active + readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected + readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary + readonly property real appOpacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + + IconImage { + id: colAppIcon + anchors.fill: parent + source: modelData.icon || "" + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && !modelData.isSteamApp && colAppIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + StyledText { + anchors.centerIn: parent + text: (modelData.fallbackText || "?").charAt(0).toUpperCase() + font.pixelSize: parent.width * 0.45 + color: appGlyphColor + font.weight: Font.Bold + } + } + + Rectangle { + anchors.fill: parent + visible: !modelData.isQuickshell && modelData.isSteamApp && colSteamIcon.status !== Image.Ready + color: Theme.surfaceContainer + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: appBorderColor + opacity: appOpacity + + DankIcon { + anchors.centerIn: parent + size: parent.width * 0.7 + name: "sports_esports" + color: appGlyphColor + } + } + + IconImage { + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isQuickshell + layer.enabled: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor) + } + } + + IconImage { + id: colSteamIcon + anchors.fill: parent + source: modelData.icon + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && modelData.icon + } + + DankIcon { + anchors.centerIn: parent + size: root.appIconSize + name: "sports_esports" + color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor + opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 + visible: modelData.isSteamApp && !modelData.icon + } + + Rectangle { + anchors.fill: parent + visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive + color: "transparent" + radius: Theme.cornerRadius * (root.appIconSize / 40) + border.width: 1 + border.color: focusedBorderColor + z: 1 + } + + MouseArea { + id: colAppMouseArea + anchors.fill: parent + enabled: isActive + cursorShape: Qt.PointingHandCursor + onClicked: { + const winId = modelData.windowId; + if (!winId) + return; + if (CompositorService.isHyprland) { + Hyprland.dispatch(`focuswindow address:${winId}`); + } else if (CompositorService.isNiri) { + NiriService.focusWindow(winId); + } + } + } + + Rectangle { + visible: modelData.count > 1 && !isActive + width: root.appIconSize * 0.67 + height: root.appIconSize * 0.67 + radius: root.appIconSize * 0.33 + color: "black" + border.color: "white" + border.width: 1 + anchors.right: parent.right + anchors.bottom: parent.bottom + z: 2 + + Text { + anchors.centerIn: parent + text: modelData.count + font.pixelSize: root.appIconSize * 0.44 + color: "white" + } + } + } + } + } + } + } + } + } + + Component.onCompleted: updateAllData() + + Connections { + target: CompositorService + function onSortedToplevelsChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: NiriService + enabled: CompositorService.isNiri + function onAllWorkspacesChanged() { + delegateRoot.updateAllData(); + } + function onWindowUrgentChanged() { + delegateRoot.updateAllData(); + } + function onWindowsChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: SettingsData + function onShowWorkspaceAppsChanged() { + delegateRoot.updateAllData(); + } + function onWorkspaceNameIconsChanged() { + delegateRoot.updateAllData(); + } + function onAppIdSubstitutionsChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: DwlService + enabled: CompositorService.isDwl + function onStateChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: Hyprland.workspaces + enabled: CompositorService.isHyprland + function onValuesChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: I3.workspaces + enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) + function onValuesChanged() { + delegateRoot.updateAllData(); + } + } + Connections { + target: ExtWorkspaceService + enabled: root.useExtWorkspace + function onStateChanged() { + delegateRoot.updateAllData(); + } + } + } + } + } + + Component.onCompleted: { + if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) { + DMSService.addSubscription("extworkspace"); + } + _updateBlurRegistration(); + } + + property bool _blurRegistered: false + readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0 + + on_ShouldBlurChanged: _updateBlurRegistration() + + function _updateBlurRegistration() { + if (_shouldBlur && !_blurRegistered) { + blurBarWindow.registerBlurWidget(visualBackground); + _blurRegistered = true; + } else if (!_shouldBlur && _blurRegistered) { + if (blurBarWindow && blurBarWindow.unregisterBlurWidget) + blurBarWindow.unregisterBlurWidget(visualBackground); + _blurRegistered = false; + } + } + + Component.onDestruction: { + if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget) + blurBarWindow.unregisterBlurWidget(visualBackground); + } +} -- cgit v1.3