1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-27 05:25:19 -04:00
Files
DankMaterialShell/quickshell/Modules/DankBar/Widgets/WorkspaceSwitcher.qml
T
Maddison Cohodas 48f6a0c632 feat(bar): separate workspace appearance for unfocused displays (#2687)
* feat(bar): separate workspace appearance for unfocused displays

Add a toggle in the Workspace Appearance card to style workspace
indicators independently on displays that aren't currently focused.

The card is split into Focused Display and Unfocused Display(s) tabs.
When the new master toggle is off (default), unfocused displays inherit
the focused settings, preserving existing behavior. When on, unfocused
displays use their own colors (focused/occupied/unfocused/urgent) and
focused border (enable, color, thickness).

WorkspaceSwitcher resolves the focused monitor per compositor and routes
color/border resolution through isFocusedMonitor/useUnfocusedAppearance.

* refactor(bar): address review feedback for unfocused workspace appearance

- Consolidate focused-monitor lookup into BarWidgetService.getFocusedScreenName(),
  adding Mango support, and drop the duplicate compositor switches in
  WorkspaceSwitcher (effectiveScreenName + isFocusedMonitor now use the helper).
- Extract the shared color and border options into reusable
  WorkspaceAppearanceColorOptions and WorkspaceAppearanceBorderFields components
  so the focused and unfocused tabs no longer duplicate the layout.
- Gate the unfocused-display options on BarWidgetService.focusedScreenDetectionSupported
  and show an explanatory note on compositors without focus detection (e.g. labwc),
  instead of silently no-opping.
2026-06-26 23:34:24 -04:00

2031 lines
95 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import Quickshell.I3
import Quickshell.WindowManager
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 bool isMango: CompositorService.isMango
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;
return BarWidgetService.getFocusedScreenName() || root.screenName;
}
readonly property bool mangoOverviewActive: CompositorService.isMango && MangoService.isOutputInOverview(effectiveScreenName)
readonly property bool isFocusedMonitor: {
const focused = BarWidgetService.getFocusedScreenName();
return focused === "" || root.screenName === "" || focused === root.screenName;
}
readonly property bool useUnfocusedAppearance: !isFocusedMonitor && SettingsData.workspaceUnfocusedMonitorSeparateAppearance && BarWidgetService.focusedScreenDetectionSupported
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
readonly property bool useExtWorkspace: {
if (Quickshell.env("DMS_FORCE_EXTWS") === "1")
return (WindowManager.windowsets?.length ?? 0) > 0;
if (!CompositorService.compositorDetected)
return false;
switch (CompositorService.compositor) {
case "niri":
case "hyprland":
case "mango":
case "sway":
case "scroll":
case "miracle":
return false;
default:
return (WindowManager.windowsets?.length ?? 0) > 0;
}
}
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 "mango":
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 (root.isMango) {
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 "mango":
if (root.mangoOverviewActive)
return [];
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 (root.isMango) {
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 (root.isMango) {
const output = MangoService.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;
}
if (CompositorService.isMango) {
// mangoTags are 1-based; targetWorkspaceId is 0-based.
if (!(w.mangoTags || []).includes(targetWorkspaceId + 1))
return;
} else {
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 groupThisWs = SettingsData.groupWorkspaceApps && (!isActiveWs || SettingsData.groupActiveWorkspaceApps);
const key = groupThisWs ? moddedId : `${moddedId}_${i}`;
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,
"_placeholder": true
};
} else if (CompositorService.isNiri) {
placeholder = {
"id": -1,
"idx": -1,
"name": ""
};
} else if (CompositorService.isHyprland) {
placeholder = {
"id": -1,
"name": ""
};
} else if (root.isMango) {
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 (!MangoService.available)
return [];
const targetScreen = root.effectiveScreenName;
const output = MangoService.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 = MangoService.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 (!MangoService.available)
return [];
return MangoService.getActiveTags(root.effectiveScreenName);
}
function getExtWorkspaceWorkspaces() {
const fallback = [
{
"id": "1",
"name": "1",
"active": false
}
];
if (!extProjection)
return fallback;
let visible = extProjection.windowsets.filter(ws => ws.shouldDisplay);
const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0);
if (hasValidCoordinates) {
visible = visible.slice().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];
});
}
return visible.length > 0 ? visible : fallback;
}
function getExtWorkspaceActiveWorkspace() {
if (!extProjection)
return "";
const activeWs = extProjection.windowsets.find(ws => ws.active);
return activeWs ? (activeWs.id || activeWs.name || "") : "";
}
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._placeholder;
if (CompositorService.isNiri)
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (root.isMango)
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) {
if (typeof data.activate === "function")
data.activate();
return;
}
switch (CompositorService.compositor) {
case "niri":
if (data.id !== undefined)
NiriService.switchToWorkspace(data.id);
break;
case "hyprland":
if (data.id) {
HyprlandService.focusWorkspace(data.id);
}
break;
case "mango":
if (data.tag !== undefined)
MangoService.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];
if (typeof nextWorkspace.activate === "function")
nextWorkspace.activate();
} 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.id === undefined) {
return;
}
NiriService.switchToWorkspace(nextWorkspace.id);
} 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;
}
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
} else if (root.isMango) {
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;
}
MangoService.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 (root.isMango)
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 (root.isMango) {
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 || root.isMango || 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
// mango reports active_tags=0 while the overview is open; surface it as a pill
Item {
id: overviewPill
visible: CompositorService.isMango && MangoService.inOverview
width: root.isVertical ? root.widgetHeight : overviewBg.width
height: root.isVertical ? overviewBg.height : root.widgetHeight
readonly property real labelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
Rectangle {
id: overviewBg
anchors.centerIn: parent
width: root.isVertical ? Math.max(root.widgetHeight * 0.7, overviewContent.implicitWidth + Theme.spacingS) : (overviewContent.implicitWidth + Theme.spacingS * 2)
height: Math.max(root.widgetHeight * 0.5, overviewContent.implicitHeight + Theme.spacingXS)
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.primary, 0.18)
Row {
id: overviewContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "grid_view"
size: overviewPill.labelSize + 2
color: Theme.primary
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
visible: !root.isVertical
text: I18n.tr("Overview")
color: Theme.primary
font.pixelSize: overviewPill.labelSize
font.weight: Font.DemiBold
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached(["mmsg", "dispatch", "toggleoverview"])
}
}
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 (root.isMango)
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 (root.isMango)
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._placeholder);
if (CompositorService.isNiri)
return !!(modelData && modelData.idx === -1);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === -1);
if (root.isMango)
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 (root.isMango)
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 (root.isMango) {
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 (CompositorService.isMango) {
if (!(w.mangoTags || []).includes(targetWorkspaceId + 1))
continue;
} else 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 || SettingsData.groupActiveWorkspaceApps)) ? 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);
}
function colorFromMode(mode, fallbackColor, customColor, customFallbackColor) {
switch (mode) {
case "primary":
case "pri":
return Theme.primary;
case "primaryContainer":
return Theme.primaryContainer;
case "secondary":
case "sec":
return Theme.secondary;
case "secondaryContainer":
return Theme.secondaryContainer;
case "tertiary":
case "ter":
return Theme.tertiary;
case "tertiaryContainer":
return Theme.tertiaryContainer;
case "surfaceText":
return Theme.surfaceText;
case "s":
return Theme.surface;
case "sc":
return Theme.surfaceContainer;
case "sch":
return Theme.surfaceContainerHigh;
case "schh":
return Theme.surfaceContainerHighest;
case "error":
case "err":
return Theme.error;
case "custom":
return Theme.safeColor(customColor, customFallbackColor);
default:
return fallbackColor;
}
}
function effectiveColorMode(focusedMode, unfocusedMode) {
return root.useUnfocusedAppearance ? unfocusedMode : focusedMode;
}
function effectiveCustomColor(focusedCustom, unfocusedCustom) {
return root.useUnfocusedAppearance ? unfocusedCustom : focusedCustom;
}
readonly property color unfocusedColor: colorFromMode(effectiveColorMode(SettingsData.workspaceUnfocusedColorMode, SettingsData.workspaceUnfocusedMonitorUnfocusedColorMode), Theme.surfaceTextAlpha, effectiveCustomColor(SettingsData.workspaceUnfocusedCustomColor, SettingsData.workspaceUnfocusedMonitorUnfocusedCustomColor), Theme.surfaceTextAlpha)
readonly property color activeColor: {
const mode = effectiveColorMode(SettingsData.workspaceColorMode, SettingsData.workspaceUnfocusedMonitorColorMode);
if (mode === "none")
return unfocusedColor;
return colorFromMode(mode, Theme.primary, effectiveCustomColor(SettingsData.workspaceFocusedCustomColor, SettingsData.workspaceUnfocusedMonitorFocusedCustomColor), Theme.primary);
}
readonly property color occupiedColor: {
const mode = effectiveColorMode(SettingsData.workspaceOccupiedColorMode, SettingsData.workspaceUnfocusedMonitorOccupiedColorMode);
if (mode === "none")
return unfocusedColor;
return colorFromMode(mode, unfocusedColor, effectiveCustomColor(SettingsData.workspaceOccupiedCustomColor, SettingsData.workspaceUnfocusedMonitorOccupiedCustomColor), Theme.secondary);
}
readonly property color urgentColor: colorFromMode(effectiveColorMode(SettingsData.workspaceUrgentColorMode, SettingsData.workspaceUnfocusedMonitorUrgentColorMode), Theme.error, effectiveCustomColor(SettingsData.workspaceUrgentCustomColor, SettingsData.workspaceUnfocusedMonitorUrgentCustomColor), Theme.error)
readonly property color focusedBorderColor: colorFromMode(effectiveColorMode(SettingsData.workspaceFocusedBorderColor, SettingsData.workspaceUnfocusedMonitorBorderColor), Theme.primary, effectiveCustomColor(SettingsData.workspaceFocusedBorderCustomColor, SettingsData.workspaceUnfocusedMonitorBorderCustomColor), Theme.primary)
readonly property bool focusedBorderEnabledForMonitor: root.useUnfocusedAppearance ? SettingsData.workspaceUnfocusedMonitorBorderEnabled : SettingsData.workspaceFocusedBorderEnabled
readonly property int focusedBorderThicknessForMonitor: root.useUnfocusedAppearance ? SettingsData.workspaceUnfocusedMonitorBorderThickness : SettingsData.workspaceFocusedBorderThickness
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.id !== undefined && targetWs.idx !== undefined) {
root.suppressShiftAnimation = true;
NiriService.moveWorkspaceToIndex(sourceWs.id, 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) {
if (typeof modelData?.activate === "function")
modelData.activate();
} else if (CompositorService.isNiri) {
if (modelData && modelData.id !== undefined) {
NiriService.switchToWorkspace(modelData.id);
}
} else if (CompositorService.isHyprland && modelData?.id) {
HyprlandService.focusWorkspace(modelData.id);
} else if (root.isMango && modelData?.tag !== undefined) {
MangoService.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 (root.isMango && modelData?.tag !== undefined) {
MangoService.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 (root.isMango) {
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 (root.isMango || 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 = (delegateRoot.focusedBorderEnabledForMonitor && isActive && !isPlaceholder) ? delegateRoot.focusedBorderThicknessForMonitor : 0;
return delegateRoot.visualWidth + borderWidth * 2;
}
height: {
const borderWidth = (delegateRoot.focusedBorderEnabledForMonitor && isActive && !isPlaceholder) ? delegateRoot.focusedBorderThicknessForMonitor : 0;
return delegateRoot.visualHeight + borderWidth * 2;
}
radius: Theme.cornerRadius
color: "transparent"
border.width: (delegateRoot.focusedBorderEnabledForMonitor && isActive && !isPlaceholder) ? delegateRoot.focusedBorderThicknessForMonitor : 0
border.color: (delegateRoot.focusedBorderEnabledForMonitor && 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) {
HyprlandService.focusWindow(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
StyledText {
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) {
HyprlandService.focusWindow(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
StyledText {
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: root
function onCurrentWorkspaceChanged() {
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();
}
function onGroupWorkspaceAppsChanged() {
delegateRoot.updateAllData();
}
function onGroupActiveWorkspaceAppsChanged() {
delegateRoot.updateAllData();
}
}
Connections {
target: MangoService
enabled: root.isMango
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();
}
}
property var _extWindowsetsTrigger: root.useExtWorkspace ? WindowManager.windowsets : null
on_ExtWindowsetsTriggerChanged: {
if (root.useExtWorkspace)
delegateRoot.updateAllData();
}
}
}
}
Component.onCompleted: {
_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);
}
}