Files

1299 lines
68 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.WindowManager
import qs.Common
import qs.Modules.DBar
import qs.Modules.Plugins
import qs.Services
import qs.DCommon.Widgets
import qs.Widgets
import "../../../DCommon/Common/WheelInput.js" as WheelInput
BasePill {
id: root
enableBackgroundHover: false
property bool isVertical: axis?.isVertical ?? false
property var widgetData: null
property string screenName: ""
property var hyprlandOverviewLoader: null
property var surfaceContext: null
readonly property string indicatorStyle: root.opt("workspaceIndicatorStyle")
readonly property bool linesStyle: indicatorStyle === "lines"
readonly property bool cardsStyle: indicatorStyle === "cards"
readonly property bool dotsStyle: indicatorStyle === "dots"
readonly property real roundness: root.opt("workspaceIndicatorRoundness")
readonly property bool compactIndicators: root.opt("workspaceIndicatorCompact")
readonly property bool segmented: !linesStyle && !cardsStyle && !dotsStyle && surfaceContext?.kind !== "dock" && BarMetrics.widgetStyle(barConfig) === "segments" && !noBackground
readonly property real compactRatio: BarMetrics.indicatorRatio(indicatorStyle, "compact", compactIndicators)
readonly property real slimRatio: BarMetrics.indicatorRatio(indicatorStyle, "slim", compactIndicators)
readonly property real activeSlimRatio: BarMetrics.indicatorRatio(indicatorStyle, "activeSlim", compactIndicators)
readonly property real activeRatio: BarMetrics.indicatorRatio(indicatorStyle, "active", compactIndicators)
readonly property real compactScale: compactIndicators ? BarMetrics.indicatorCompactScale : 1
readonly property real activeIconRatio: 1.6 * compactScale
readonly property real iconRatio: 1.2 * compactScale
readonly property real overviewTintAlpha: 0.18
readonly property real dragOpacity: 0.8
readonly property real hoverFadeAlpha: 0.7
readonly property real cardWindowRatio: 0.45
WorkspaceSwitcherModel {
id: workspaces
screenName: root.screenName
screen: root.parentScreen
followFocus: root.opt("workspaceFollowFocus")
occupiedOnly: root.opt("showOccupiedWorkspacesOnly")
showAllTags: root.opt("dwlShowAllTags")
showPadding: root.opt("showWorkspacePadding")
paddingCount: root.opt("workspacePaddingCount")
showSpecial: root.opt("showSpecialWorkspaces")
reverseScrolling: root.opt("reverseScrolling")
}
readonly property bool useAqueous: workspaces.useAqueous
readonly property bool useExtWorkspace: workspaces.useExtWorkspace
readonly property bool useNativeWorkspaces: workspaces.useNativeWorkspaces
readonly property string effectiveScreenName: workspaces.effectiveScreenName
readonly property bool workspacesHiddenByOverview: workspaces.hiddenByOverview
readonly property var currentWorkspace: workspaces.currentWorkspace
readonly property var workspaceList: workspaces.workspaceList
function opt(key) {
return SettingsData.widgetOption("workspaceSwitcher", widgetData, key);
}
property int _desktopEntriesUpdateTrigger: 0
readonly property bool isFocusedMonitor: {
const focused = BarWidgetService.getFocusedScreenName();
return focused === "" || root.screenName === "" || focused === root.screenName;
}
readonly property bool useUnfocusedAppearance: !isFocusedMonitor && root.opt("workspaceUnfocusedMonitorSeparateAppearance") && BarWidgetService.focusedScreenDetectionSupported
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++;
}
}
function getWorkspaceIcons(ws) {
_desktopEntriesUpdateTrigger;
if (!root.opt("showWorkspaceApps") || !ws || !root.useNativeWorkspaces || ws.placeholder) {
return [];
}
const byApp = {};
const isActiveWs = CompositorService.workspaceAppsActive(ws, root.currentWorkspace);
CompositorService.windowsOnWorkspace(ws).forEach((w, i) => {
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
const moddedId = Paths.moddedAppId(keyBase);
const groupThisWs = root.opt("groupWorkspaceApps") && (!isActiveWs || root.opt("groupActiveWorkspaceApps"));
const key = groupThisWs ? moddedId : (w.aqueousKey || `${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),
"count": 1,
"windowId": w.address || w.id,
"windowSession": useAqueous ? w.aqueousSession : "",
"fallbackText": appName || ""
};
} else {
byApp[key].count++;
if (w.activated || w.is_focused) {
byApp[key].active = true;
}
}
});
return Object.values(byApp);
}
function recordOf(entry) {
return workspaces.recordOf(entry);
}
readonly property real appIconSize: Theme.barIconSize(barThickness, -6 + root.opt("workspaceAppIconSizeOffset"), root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
function getRealWorkspaces() {
return workspaces.realWorkspaces;
}
function switchToWorkspaceByModelData(entry) {
workspaces.switchTo(entry);
}
function toggleHyprlandOverview() {
const overview = root.hyprlandOverviewLoader?.item;
if (!overview)
return;
overview.overviewOpen = !overview.overviewOpen;
}
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 || item.isPlaceholder)
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) {
workspaces.step(direction);
}
function getWorkspaceIndexFallback(modelData, index) {
if (root.useExtWorkspace)
return index + 1;
if (!root.useNativeWorkspaces)
return modelData - 1;
return modelData?.idx ?? modelData?.name ?? "";
}
function getWorkspaceIndex(modelData, index) {
if (modelData?.placeholder === true)
return index + 1;
let workspaceName = "";
if (root.opt("showWorkspaceName")) {
workspaceName = modelData?.name ?? "";
if (workspaceName && workspaceName !== "") {
if (root.isVertical) {
workspaceName = workspaceName.charAt(0);
}
} else {
workspaceName = "";
}
}
if (workspaceName) {
if (root.opt("showWorkspaceIndex")) {
const indexLabel = getWorkspaceIndexFallback(modelData, index);
return indexLabel ? `${indexLabel}: ${workspaceName}` : workspaceName;
}
return workspaceName;
}
return getWorkspaceIndexFallback(modelData, index);
}
readonly property bool shouldShow: workspaces.available
width: shouldShow ? (isVertical ? barThickness : visualWidth) : 0
height: shouldShow ? (isVertical ? visualHeight : barThickness) : 0
visible: shouldShow
content: Component {
Item {
implicitWidth: workspaceRow.implicitWidth
implicitHeight: workspaceRow.implicitHeight
}
}
onRightClicked: {
CompositorService.workspaceSecondaryAction(null, root.effectiveScreenName);
root.toggleHyprlandOverview();
}
onPressedAt: (rootX, rootY) => {
const idx = root.findClosestWorkspaceIndex(rootX, rootY);
if (idx >= 0)
root.switchToWorkspaceByModelData(root.workspaceList[idx]);
}
onWheel: wheel => {
if (WheelInput.isHorizontal(wheel))
return;
wheel.accepted = true;
workspaces.handleWheel(wheel);
}
property int dragSourceIndex: -1
property int dragTargetIndex: -1
property bool suppressShiftAnimation: false
onWorkspaceListChanged: {
if (dragSourceIndex >= 0) {
dragSourceIndex = -1;
dragTargetIndex = -1;
suppressShiftAnimation = false;
}
}
BarSegment {
id: workspaceRow
anchors.centerIn: root.visualContent
spacing: root.segmented ? BarMetrics.segmentGap : Theme.spacingS
vertical: root.isVertical
// mango reports active_tags=0 while the overview is open; surface it as a pill
Item {
id: overviewPill
visible: CompositorService.workspacesHiddenByOverview(root.effectiveScreenName)
width: root.isVertical ? root.widgetThickness : overviewBg.width
height: root.isVertical ? overviewBg.height : root.widgetThickness
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.widgetThickness * root.compactRatio, overviewContent.implicitWidth + Theme.spacingS) : (overviewContent.implicitWidth + Theme.spacingS * 2)
height: Math.max(root.widgetThickness * root.slimRatio, overviewContent.implicitHeight + Theme.spacingXS)
radius: BarMetrics.pillRadius(Math.min(width, height), BarMetrics.widgetStyle(root.barConfig))
color: Theme.withAlpha(Theme.primary, root.overviewTintAlpha)
Row {
id: overviewContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DIcon {
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", "noun, niri compositor overview mode label")
color: Theme.primary
font.pixelSize: overviewPill.labelSize
font.weight: Theme.fontWeightMedium
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: CompositorService.toggleOverview(root.effectiveScreenName)
}
}
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 + workspaceRow.spacing;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
readonly property var shiftSpringParams: Theme.springPreset("fast", 150)
SpringMotion {
id: shiftXSpring
enabled: !root.suppressShiftAnimation
positionEpsilon: 0.05
velocityEpsilon: 0.05
stiffness: delegateRoot.shiftSpringParams.stiffness
damping: delegateRoot.shiftSpringParams.damping
value: delegateRoot.shiftOffset
Component.onCompleted: snapTo(delegateRoot.shiftOffset)
}
SpringMotion {
id: shiftYSpring
enabled: !root.suppressShiftAnimation
positionEpsilon: 0.05
velocityEpsilon: 0.05
stiffness: delegateRoot.shiftSpringParams.stiffness
damping: delegateRoot.shiftSpringParams.damping
value: delegateRoot.shiftOffset
Component.onCompleted: snapTo(delegateRoot.shiftOffset)
}
onShiftOffsetChanged: {
shiftXSpring.retarget(shiftOffset);
shiftYSpring.retarget(shiftOffset);
}
transform: Translate {
x: root.isVertical ? 0 : shiftXSpring.value
y: root.isVertical ? shiftYSpring.value : 0
}
readonly property var record: root.recordOf(modelData)
property bool isActive: {
if (root.useExtWorkspace || !root.useNativeWorkspaces)
return modelData === root.currentWorkspace;
return CompositorService.isCurrentWorkspace(record, root.currentWorkspace);
}
property bool isOccupied: root.useNativeWorkspaces && CompositorService.workspaceOccupied(record)
property bool isPlaceholder: !!(record && record.placeholder)
property bool isHovered: mouseArea.containsMouse
property bool loadedIsUrgent: false
property bool isUrgent: root.useNativeWorkspaces ? CompositorService.workspaceUrgent(record, loadedIsUrgent) : (modelData?.urgent ?? false)
readonly property var loadedIconData: {
if (isPlaceholder)
return null;
const name = record?.name;
if (!name)
return null;
const custom = SettingsData.getWorkspaceNameIcon(name);
if (custom || record.special !== true)
return custom;
return {
"type": "icon",
"value": "inbox"
};
}
readonly property bool loadedHasIcon: loadedIconData !== null
property var loadedIcons: []
readonly property int stableIconCount: {
if (!root.opt("showWorkspaceApps") || isPlaceholder)
return 0;
if (root.useAqueous)
return loadedIcons.length;
if (!root.useNativeWorkspaces)
return 0;
const wins = CompositorService.windowsOnWorkspace(delegateRoot.record);
const seen = {};
let groupedCount = 0;
for (let i = 0; i < wins.length; i++) {
const w = wins[i];
const appKey = w.app_id || w.appId || w.class || w.windowClass || "unknown";
if (!seen[appKey]) {
seen[appKey] = true;
groupedCount++;
}
}
return (root.opt("groupWorkspaceApps") && (!isActive || root.opt("groupActiveWorkspaceApps"))) ? groupedCount : wins.length;
}
readonly property real lineThickness: Math.max(Theme.spacingXXS, root.widgetThickness * (isActive ? root.activeSlimRatio : root.slimRatio))
// a label scales the whole shape up and itself down to fit, so every style keeps its proportions
readonly property bool labeled: !root.linesStyle && !root.opt("showWorkspaceApps") && (root.opt("showWorkspaceIndex") || root.opt("showWorkspaceName") || loadedHasIcon)
readonly property real slimBase: widgetThickness * (isActive ? root.activeSlimRatio : root.slimRatio)
readonly property real labelScale: labeled ? Math.max(BarMetrics.indicatorLabelScale, BarMetrics.indicatorLabelMin / BarMetrics.indicatorLabelRatio / slimBase) : 1
readonly property real crossBase: root.opt("showWorkspaceApps") ? Math.max(widgetThickness * root.compactRatio, root.appIconSize + Theme.spacingXS * 2) : slimBase * labelScale
readonly property real primaryBase: root.dotsStyle ? crossBase : labelScale * (isActive ? Math.max(root.widgetThickness * root.activeRatio, root.appIconSize * root.activeIconRatio) : Math.max(root.widgetThickness * root.compactRatio, root.appIconSize * root.iconRatio))
readonly property real labelSize: {
const barSize = Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText);
return labeled ? Math.min(barSize, Math.round(crossBase * BarMetrics.indicatorLabelRatio)) : barSize;
}
readonly property real baseWidth: root.isVertical ? (root.linesStyle ? lineThickness : crossBase) : primaryBase
readonly property real baseHeight: root.isVertical ? primaryBase : (root.linesStyle ? lineThickness : crossBase)
readonly property bool hasWorkspaceName: root.opt("showWorkspaceName") && record?.name && record.name !== ""
readonly property real contentImplicitWidth: appIconsLoader.item?.contentWidth ?? 0
readonly property real contentImplicitHeight: appIconsLoader.item?.contentHeight ?? 0
// lines can't hold content, so it sits beside the line as an M3 tab underline facing the screen
readonly property bool underline: root.linesStyle && contentImplicitWidth > 0 && contentImplicitHeight > 0
readonly property bool lineAfterContent: root.axis?.edge !== "bottom" && root.axis?.edge !== "right"
readonly property real underlineContentShift: underline ? (lineThickness + Theme.spacingXXS) / 2 * (lineAfterContent ? -1 : 1) : 0
readonly property real underlineLineShift: underline ? ((root.isVertical ? contentImplicitWidth : contentImplicitHeight) + Theme.spacingXXS) / 2 * (lineAfterContent ? 1 : -1) : 0
readonly property real iconsExtraWidth: {
if (!root.isVertical && root.opt("showWorkspaceApps") && stableIconCount > 0) {
const numIcons = Math.min(stableIconCount, root.opt("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 && root.opt("showWorkspaceApps") && stableIconCount > 0) {
const numIcons = Math.min(stableIconCount, root.opt("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 || (underline && root.isVertical))
return baseWidth + iconsExtraWidth;
const padding = root.isVertical ? Theme.spacingXS : Theme.spacingS;
return Math.max(baseWidth + iconsExtraWidth, contentImplicitWidth + padding);
}
// the row reserves icon height for each label, so a horizontal label must not set the thickness
readonly property real visualHeight: {
if (labeled && !root.isVertical)
return baseHeight;
if (contentImplicitHeight <= 0 || (underline && !root.isVertical))
return baseHeight + iconsExtraHeight;
const padding = root.isVertical && !labeled ? 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(root.opt("workspaceUnfocusedColorMode"), root.opt("workspaceUnfocusedMonitorUnfocusedColorMode")), Theme.surfaceTextAlpha, effectiveCustomColor(root.opt("workspaceUnfocusedCustomColor"), root.opt("workspaceUnfocusedMonitorUnfocusedCustomColor")), Theme.surfaceTextAlpha)
readonly property string activeColorMode: effectiveColorMode(root.opt("workspaceColorMode"), root.opt("workspaceUnfocusedMonitorColorMode"))
readonly property color activeColor: {
if (activeColorMode === "none")
return unfocusedColor;
return colorFromMode(activeColorMode, Theme.primary, effectiveCustomColor(root.opt("workspaceFocusedCustomColor"), root.opt("workspaceUnfocusedMonitorFocusedCustomColor")), Theme.primary);
}
readonly property color occupiedColor: {
const mode = effectiveColorMode(root.opt("workspaceOccupiedColorMode"), root.opt("workspaceUnfocusedMonitorOccupiedColorMode"));
if (mode === "none")
return unfocusedColor;
return colorFromMode(mode, unfocusedColor, effectiveCustomColor(root.opt("workspaceOccupiedCustomColor"), root.opt("workspaceUnfocusedMonitorOccupiedCustomColor")), Theme.secondary);
}
readonly property string urgentColorMode: effectiveColorMode(root.opt("workspaceUrgentColorMode"), root.opt("workspaceUnfocusedMonitorUrgentColorMode"))
readonly property color urgentColor: colorFromMode(urgentColorMode, Theme.error, effectiveCustomColor(root.opt("workspaceUrgentCustomColor"), root.opt("workspaceUnfocusedMonitorUrgentCustomColor")), Theme.error)
readonly property color focusedBorderColor: colorFromMode(effectiveColorMode(root.opt("workspaceFocusedBorderColor"), root.opt("workspaceUnfocusedMonitorBorderColor")), Theme.primary, effectiveCustomColor(root.opt("workspaceFocusedBorderCustomColor"), root.opt("workspaceUnfocusedMonitorBorderCustomColor")), Theme.primary)
readonly property bool focusedBorderEnabledForMonitor: root.useUnfocusedAppearance ? root.opt("workspaceUnfocusedMonitorBorderEnabled") : root.opt("workspaceFocusedBorderEnabled")
readonly property int focusedBorderThicknessForMonitor: root.useUnfocusedAppearance ? root.opt("workspaceUnfocusedMonitorBorderThickness") : root.opt("workspaceFocusedBorderThickness")
function getContrastingIconColor(bgColor) {
return Theme.isLightColor(bgColor, 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)
function inkFromMode(mode, fill, fallbackInk) {
switch (mode) {
case "primary":
case "pri":
return Theme.onPrimary;
case "primaryContainer":
return Theme.onPrimaryContainer;
case "secondaryContainer":
return Theme.onSecondaryContainer;
case "tertiaryContainer":
return Theme.onTertiaryContainer;
case "error":
case "err":
return Theme.onError;
case "s":
case "sc":
case "sch":
case "schh":
return Theme.onSurface;
case "secondary":
case "sec":
case "tertiary":
case "ter":
case "surfaceText":
case "custom":
case "none":
return getContrastingIconColor(fill);
default:
return fallbackInk;
}
}
readonly property color filledInk: isActive ? inkFromMode(activeColorMode, activeColor, Theme.onPrimary) : inkFromMode(urgentColorMode, urgentColor, Theme.onError)
readonly property color requestedColor: isActive ? activeColor : isUrgent ? urgentColor : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.withAlpha(unfocusedColor, root.hoverFadeAlpha) : isOccupied ? occupiedColor : unfocusedColor
property bool colorAnimationReady: false
readonly property color displayColor: pillColor.value
DColorAnimation {
id: pillColor
to: delegateRoot.requestedColor
animated: delegateRoot.colorAnimationReady
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
readonly property var rootWorkspaceList: root.workspaceList
onRootWorkspaceListChanged: {
if (!dragging)
return;
dragging = false;
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.workspaceReorderSupported && root.opt("workspaceDragReorder") && !isPlaceholder) {
mousePressed = true;
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
}
}
onPositionChanged: mouse => {
if (!mousePressed || !CompositorService.workspaceReorderSupported || !root.opt("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) + workspaceRow.spacing;
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;
CompositorService.moveWorkspace(sourceWs, targetWs);
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 (delegateRoot.focusWindowAt(mouse.x, mouse.y))
return;
root.switchToWorkspaceByModelData(modelData);
} else if (mouse.button === Qt.RightButton) {
CompositorService.workspaceSecondaryAction(record, root.effectiveScreenName);
root.toggleHyprlandOverview();
}
}
}
Timer {
id: dataUpdateTimer
interval: 50
onTriggered: {
if (isPlaceholder) {
delegateRoot.loadedIcons = [];
delegateRoot.loadedIsUrgent = false;
return;
}
delegateRoot.loadedIsUrgent = CompositorService.loadWorkspaceUrgent(delegateRoot.record);
delegateRoot.loadedIcons = root.opt("showWorkspaceApps") ? root.getWorkspaceIcons(delegateRoot.record) : [];
}
}
function updateAllData() {
dataUpdateTimer.restart();
}
function windowIdAt(x, y) {
const layout = appIconsLoader.item?.iconsLayout;
if (!layout)
return null;
const point = layout.mapFromItem(mouseArea, x, y);
const icon = layout.childAt(point.x, point.y);
if (root.useAqueous)
return icon?.windowId ? {
id: icon.windowId,
session: icon.windowSession
} : null;
return icon?.windowId ?? null;
}
function focusWindowAt(x, y) {
const winId = delegateRoot.windowIdAt(x, y);
if (!winId)
return false;
return CompositorService.focusWindow(winId);
}
width: root.isVertical ? root.widgetThickness : visualWidth
height: root.isVertical ? visualHeight : root.widgetThickness
readonly property real outlineWidth: dragHandler.dragging || isUrgent || isDropTarget ? Theme.outlineWidthFocused : 0
readonly property color outlineColor: dragHandler.dragging ? Theme.primary : (isUrgent ? urgentColor : (isDropTarget ? Theme.primary : Theme.withAlpha(Theme.primary, 0)))
Behavior on width {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on height {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Rectangle {
id: focusedBorderRing
x: root.isVertical ? (root.widgetThickness - width) / 2 + delegateRoot.underlineLineShift : (parent.width - width) / 2
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetThickness - height) / 2 + delegateRoot.underlineLineShift
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;
}
topLeftRadius: visualContent.topLeftRadius + border.width
topRightRadius: visualContent.topRightRadius + border.width
bottomLeftRadius: visualContent.bottomLeftRadius + border.width
bottomRightRadius: visualContent.bottomRightRadius + border.width
color: "transparent"
border.width: (delegateRoot.focusedBorderEnabledForMonitor && isActive && !isPlaceholder) ? delegateRoot.focusedBorderThicknessForMonitor : 0
border.color: (delegateRoot.focusedBorderEnabledForMonitor && isActive && !isPlaceholder) ? focusedBorderColor : Theme.withAlpha(focusedBorderColor, 0)
Behavior on width {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on height {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on border.width {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on border.color {
enabled: !SettingsData.reduceMotion
ColorAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
BarPillSurface {
id: visualContent
width: delegateRoot.visualWidth
height: delegateRoot.visualHeight
x: root.isVertical ? (root.widgetThickness - width) / 2 + delegateRoot.underlineLineShift : (parent.width - width) / 2
y: root.isVertical ? (parent.height - height) / 2 : (root.widgetThickness - height) / 2 + delegateRoot.underlineLineShift
thickness: Math.min(width, height)
style: BarMetrics.widgetStyle(root.barConfig)
vertical: root.isVertical
joinedStart: root.segmented && index > 0
joinedEnd: root.segmented && index < workspaceRepeater.count - 1
pressed: mouseArea.pressed
color: root.cardsStyle && !isActive ? "transparent" : delegateRoot.displayColor
radiusOverride: BarMetrics.indicatorRadius(root.indicatorStyle, thickness, root.roundness)
opacity: dragHandler.dragging ? root.dragOpacity : 1.0
border.width: root.cardsStyle && !isActive ? Math.max(Theme.outlineWidth, delegateRoot.outlineWidth) : delegateRoot.outlineWidth
border.color: delegateRoot.outlineWidth > 0 ? delegateRoot.outlineColor : delegateRoot.displayColor
// an empty frame is an empty desktop; a window block marks it occupied
Rectangle {
anchors.centerIn: parent
visible: root.cardsStyle && isOccupied && !isActive && !appIconsLoader.active
width: Math.round(parent.width * root.cardWindowRatio)
height: Math.round(parent.height * root.cardWindowRatio)
radius: Math.min(Theme.cornerRadiusXXS, height / 2)
color: delegateRoot.displayColor
}
transform: Translate {
x: root.isVertical ? 0 : (dragHandler.dragging ? dragHandler.dragAxisOffset : 0)
y: root.isVertical ? (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) : 0
}
Behavior on opacity {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Behavior on width {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on height {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on border.width {
enabled: !SettingsData.reduceMotion
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Behavior on border.color {
enabled: !SettingsData.reduceMotion
ColorAnimation {
duration: Theme.expressiveDurations.expressiveEffects
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
Loader {
id: appIconsLoader
anchors.fill: parent
active: root.opt("showWorkspaceApps") || root.opt("showWorkspaceIndex") || root.opt("showWorkspaceName") || loadedHasIcon
opacity: visualContent.opacity
transform: Translate {
x: root.isVertical ? 0 : (dragHandler.dragging ? dragHandler.dragAxisOffset : 0)
y: root.isVertical ? (dragHandler.dragging ? dragHandler.dragAxisOffset : 0) : 0
}
sourceComponent: Item {
id: contentRoot
readonly property real contentWidth: contentRow.item?.implicitWidth ?? 0
readonly property real contentHeight: contentRow.item?.implicitHeight ?? 0
property alias iconsLayout: contentRow.item
Loader {
id: contentRow
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.isVertical ? delegateRoot.underlineContentShift : 0
anchors.verticalCenterOffset: root.isVertical ? 0 : delegateRoot.underlineContentShift
sourceComponent: root.isVertical ? columnLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: Theme.spacingXS
visible: loadedIcons.length > 0 || root.opt("showWorkspaceIndex") || root.opt("showWorkspaceName") || loadedHasIcon
Item {
visible: loadedHasIcon && loadedIconData?.type === "icon"
width: wsIcon.width
height: root.appIconSize
DIcon {
id: wsIcon
anchors.verticalCenter: parent.verticalCenter
name: loadedIconData?.value ?? ""
size: labelSize
color: (isActive || isUrgent) ? filledInk : 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) ? filledInk : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: labelSize
font.weight: (isActive && !isPlaceholder) ? Theme.fontWeightMedium : Theme.fontWeight
}
}
Item {
visible: ((root.opt("showWorkspaceIndex") || root.opt("showWorkspaceName")) && !loadedHasIcon) || (loadedHasIcon && root.opt("showWorkspaceName") && hasWorkspaceName)
width: wsIndexText.implicitWidth
height: root.appIconSize
StyledText {
id: wsIndexText
anchors.verticalCenter: parent.verticalCenter
text: loadedHasIcon ? (record?.name ?? "") : root.getWorkspaceIndex(record, index)
color: (isActive || isUrgent) ? filledInk : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: labelSize
font.weight: (isActive && !isPlaceholder) ? Theme.fontWeightMedium : Theme.fontWeight
}
}
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, root.opt("maxWorkspaceIcons"))
}
delegate: Item {
width: root.appIconSize
height: root.appIconSize
readonly property var windowId: modelData.windowId
readonly property string windowSession: modelData.windowSession || ""
readonly property bool appHighlightActive: root.opt("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 : rowAppHover.hovered ? 0.8 : 0.6
IconImage {
id: rowAppIcon
anchors.fill: parent
source: modelData.icon || ""
opacity: modelData.active ? 1.0 : rowAppHover.hovered ? 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.chipSurface
radius: Math.min(Theme.cornerRadiusS, width / 2, height / 2)
border.width: Theme.outlineWidth
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: Theme.fontWeightMedium
}
}
Rectangle {
anchors.fill: parent
visible: !modelData.isQuickshell && modelData.isSteamApp && rowSteamIcon.status !== Image.Ready
color: Theme.chipSurface
radius: Math.min(Theme.cornerRadiusS, width / 2, height / 2)
border.width: Theme.outlineWidth
border.color: appBorderColor
opacity: appOpacity
DIcon {
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 : rowAppHover.hovered ? 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 : rowAppHover.hovered ? 0.8 : 0.6
visible: modelData.isSteamApp && modelData.icon
}
DIcon {
anchors.centerIn: parent
size: root.appIconSize
name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
opacity: modelData.active ? 1.0 : rowAppHover.hovered ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon
}
Rectangle {
anchors.fill: parent
visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Math.min(Theme.cornerRadiusS, width / 2, height / 2)
border.width: Theme.outlineWidth
border.color: focusedBorderColor
z: 1
}
HoverHandler {
id: rowAppHover
}
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: Theme.outlineWidth
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: Theme.spacingXS
visible: loadedIcons.length > 0 || root.opt("showWorkspaceIndex") || root.opt("showWorkspaceName") || loadedHasIcon
DIcon {
visible: loadedHasIcon && loadedIconData?.type === "icon"
anchors.horizontalCenter: parent.horizontalCenter
name: loadedIconData?.value ?? ""
size: labelSize
color: (isActive || isUrgent) ? filledInk : 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) ? filledInk : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: labelSize
font.weight: (isActive && !isPlaceholder) ? Theme.fontWeightMedium : Theme.fontWeight
}
StyledText {
visible: (root.opt("showWorkspaceIndex") || root.opt("showWorkspaceName")) && !loadedHasIcon
anchors.horizontalCenter: parent.horizontalCenter
text: root.getWorkspaceIndex(record, index)
color: (isActive || isUrgent) ? filledInk : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: labelSize
font.weight: (isActive && !isPlaceholder) ? Theme.fontWeightMedium : Theme.fontWeight
}
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, root.opt("maxWorkspaceIcons"))
}
delegate: Item {
width: root.appIconSize
height: root.appIconSize
readonly property var windowId: modelData.windowId
readonly property string windowSession: modelData.windowSession || ""
readonly property bool appHighlightActive: root.opt("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 : colAppHover.hovered ? 0.8 : 0.6
IconImage {
id: colAppIcon
anchors.fill: parent
source: modelData.icon || ""
opacity: modelData.active ? 1.0 : colAppHover.hovered ? 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.chipSurface
radius: Math.min(Theme.cornerRadiusS, width / 2, height / 2)
border.width: Theme.outlineWidth
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: Theme.fontWeightMedium
}
}
Rectangle {
anchors.fill: parent
visible: !modelData.isQuickshell && modelData.isSteamApp && colSteamIcon.status !== Image.Ready
color: Theme.chipSurface
radius: Math.min(Theme.cornerRadiusS, width / 2, height / 2)
border.width: Theme.outlineWidth
border.color: appBorderColor
opacity: appOpacity
DIcon {
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 : colAppHover.hovered ? 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 : colAppHover.hovered ? 0.8 : 0.6
visible: modelData.isSteamApp && modelData.icon
}
DIcon {
anchors.centerIn: parent
size: root.appIconSize
name: "sports_esports"
color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
opacity: modelData.active ? 1.0 : colAppHover.hovered ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon
}
Rectangle {
anchors.fill: parent
visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Math.min(Theme.cornerRadiusS, width / 2, height / 2)
border.width: Theme.outlineWidth
border.color: focusedBorderColor
z: 1
}
HoverHandler {
id: colAppHover
}
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: Theme.outlineWidth
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();
delegateRoot.colorAnimationReady = true;
}
Connections {
target: CompositorService
function onSortedToplevelsChanged() {
delegateRoot.updateAllData();
}
function onWorkspaceStateChanged() {
delegateRoot.updateAllData();
}
}
readonly property var rootCurrentWorkspace: root.currentWorkspace
onRootCurrentWorkspaceChanged: updateAllData()
Connections {
target: SettingsData
function onBarConfigsChanged() {
delegateRoot.updateAllData();
}
function onWorkspaceNameIconsChanged() {
delegateRoot.updateAllData();
}
function onAppIdSubstitutionsChanged() {
delegateRoot.updateAllData();
}
}
property var _extWindowsetsTrigger: root.useExtWorkspace ? WindowManager.windowsets : null
on_ExtWindowsetsTriggerChanged: {
if (root.useExtWorkspace)
delegateRoot.updateAllData();
}
}
}
}
}