// SPDX-FileCopyrightText: 2021 Devin Lin // SPDX-FileCopyrightText: 2025 Luis Büchi // SPDX-License-Identifier: GPL-2.0-or-later pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.kwin 3.0 as KWinComponents MouseArea { id: root readonly property int count: repeater.count required property real shellTopMargin required property real shellBottomMargin required property var taskSwitcher readonly property var taskSwitcherState: taskSwitcher.state readonly property var taskSwitcherHelpers: taskSwitcher.taskSwitcherHelpers property int taskInteractingCount: 0 // account for system header and footer offset (center the preview image) // if there's too little space space for the task scrub icons, shift it slightly above center to make space readonly property real taskYBase: { let headerHeight = shellTopMargin; let footerHeight = shellBottomMargin; let diff = headerHeight - footerHeight; let baseY = (taskSwitcher.height / 2) - (taskSwitcherHelpers.taskHeight / 2) - (taskSwitcherHelpers.taskHeaderHeight / 2); return baseY + diff / 2 - shellTopMargin; } readonly property real taskY: { let trackFingerYOffsetClamped = 0; if (taskSwitcherHelpers.isScaleClamped && (taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened)) { trackFingerYOffsetClamped = taskSwitcherHelpers.trackFingerYOffset; } let scrubModeOffset = 0; if (taskSwitcherHelpers.isInTaskScrubMode && !taskSwitcherHelpers.currentlyBeingClosed) { scrubModeOffset = taskSwitcherHelpers.scrubModeOverrun; } return Math.round(taskYBase - trackFingerYOffsetClamped - scrubModeOffset); } function getTaskAt(index: int): Task { return repeater.itemAt(index); } function closeAll(): void { for (let i = 0; i < repeater.count; i++) { repeater.itemAt(i).closeApp(); } } function minimizeAll(): void { for (let i = 0; i < repeater.count; i++) { let item = repeater.itemAt(i); // minimize window if (!item.window.minimized) { item.minimizeApp(); } } } function jumpToFirstVisibleWindow(): void { for (let i = 0; i < repeater.count; i++) { let item = repeater.itemAt(i); if (!item.window.minimized) { taskSwitcherHelpers.goToTaskIndex(i); break; } } } // the position offset value for non-active tasks in the task drawer // this value is normalized and is usually set to 0, 1, or 2 (larger the number, the further they are from the active task) property real baseTaskOffset: 0 // the position offset value tracked to the touch x position when opening the task drawer from the home screen // this allows the task switcher to move left and right without causing problems with the task offset animations property real homeTouchPositionX: 0 // the touch x position value normalized between 0 and 1 // the base value should be 0.5 for when it is on the home screen readonly property real touchPosition: { let value = 0.5 if (taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened) { // since the touch position starts at 0, we add half the window width and then divide it by the full width to normalize it value = ((taskSwitcherHelpers.notHomeScreenState ? taskSwitcherState.touchXPosition : 0) + (taskSwitcherHelpers.windowWidth / 2)) / taskSwitcherHelpers.windowWidth value = Math.min(1, Math.max(0, value)) } return value } // dynamic task offset animation duration based off of the touch position and task scale function dynamicDuration(left = true): int { // if the close animation is running, use the standard long duration time for consistency let duration = Kirigami.Units.longDuration * 1.75 if (!taskSwitcherHelpers.closeAnim.running && taskSwitcherHelpers.notHomeScreenState && taskSwitcherHelpers.gestureState != TaskSwitcherHelpers.GestureStates.HorizontalSwipe && !taskSwitcherHelpers.isInTaskScrubMode) { // max out the scale at 1 so it is not too fast when opening the task drawer with the button let taskScale = Math.min(taskSwitcherHelpers.currentScale, 1) // change the duration based off of the touch position and task scale duration = duration * ((left ? touchPosition : (1 - touchPosition)) + 1) } return duration } // the duration is set to 0 if setOffsetDurationImmediately is true so we can skip the animation readonly property real taskOffsetDurationLeft: setOffsetDurationImmediately ? 0 : dynamicDuration(true) readonly property real taskOffsetDurationRight: setOffsetDurationImmediately ? 0 : dynamicDuration(false) // the easing type for the task offset animation property int taskOffsetEasing: Easing.InOutQuart // skips the animation and sets the task offset value immediately (only should be set by 'setTaskOffsetValue') property bool setOffsetDurationImmediately: true // set the task offset value with an animation unless specified otherwise function setTaskOffsetValue(value: int, immediately = false, taskEasing = ((taskSwitcherHelpers.notHomeScreenState || (value != 0)) && (baseTaskOffset != taskSwitcherHelpers.taskOffsetValue)) ? Easing.InOutQuart : Easing.OutQuart): void { if (baseTaskOffset == value && immediately) { baseTaskOffset = value + 1; } setOffsetDurationImmediately = immediately; taskOffsetEasing = taskEasing; baseTaskOffset = value; } transform: Scale { origin.x: root.width / 2 origin.y: root.height / 2 xScale: root.taskSwitcherHelpers.currentScale yScale: root.taskSwitcherHelpers.currentScale } onClicked: { // if tapped on the background, then hide taskSwitcher.hide(); } onPressedChanged: { // disable if being closed or opened to prevent bugs if (!taskSwitcherHelpers.currentlyBeingOpened && !taskSwitcherHelpers.currentlyBeingClosed && pressed) { // ensure animations aren't running when finger is pressed taskSwitcherHelpers.cancelAnimations(); } } Repeater { id: repeater model: root.taskSwitcher.tasksModel // left margin from root edge such that the task is centered readonly property real leftMargin: (root.width / 2) - (root.taskSwitcherHelpers.taskWidth / 2) delegate: Task { id: task readonly property int currentIndex: model.index readonly property bool isCurrentTask: currentIndex == root.taskSwitcherHelpers.currentDisplayTask // this is the x-position with respect to the list property real listX: root.taskSwitcherHelpers.xPositionFromTaskIndex(currentIndex) Behavior on listX { NumberAnimation { duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad } } // the animated task offset value (always will be 0 if it is the current task in the task drawer) property real taskOffsetNormalized: (root.baseTaskOffset * ((root.taskSwitcherHelpers.taskDrawerOpened && isCurrentTask) ? 0 : 1)) Behavior on taskOffsetNormalized { NumberAnimation { duration: root.taskSwitcherHelpers.currentDisplayTask > task.currentIndex ? root.taskOffsetDurationRight : root.taskOffsetDurationLeft easing.type: root.taskOffsetEasing easing.overshoot: 0.85 } } // calculate which direction to offset readonly property real offsetDir: (root.taskSwitcherHelpers.currentDisplayTask > currentIndex ? -1 : 1) // check if this task should be offset readonly property real isOffScreenOffset: { let isOffsetBase = ((!root.taskSwitcherState.wasInActiveTask && !root.taskSwitcherHelpers.taskDrawerOpened) || !isCurrentTask) ? (root.taskSwitcherHelpers.isInTaskScrubMode && root.taskSwitcherHelpers.notHomeScreenState ? 0 : 1) : 0 let isOffsetTaskDrawer = (currentIndex == root.taskSwitcherHelpers.currentDisplayTask ? 0 : 1) return root.taskSwitcherHelpers.taskDrawerOpened ? isOffsetTaskDrawer : isOffsetBase } // how far the task needs to travel to be off screen readonly property real scrollXOffset: Math.abs(root.taskSwitcherHelpers.xPositionFromTaskIndex(root.taskSwitcherHelpers.currentDisplayTask) - (root.taskSwitcherState.xPosition + (root.taskSwitcherState.touchXPosition / root.taskSwitcherHelpers.currentScale))) readonly property real offScreenOffset: (root.taskSwitcherHelpers.windowWidth * (((root.taskSwitcherHelpers.notHomeScreenState ? root.taskSwitcherState.touchXPosition : 0) * offsetDir * ((root.homeTouchPositionX == 0) ? 1 : 0) + (root.taskSwitcherHelpers.windowWidth / 2)) / root.taskSwitcherHelpers.windowWidth)); // calculate the actual task offset readonly property real taskOffset: ((offScreenOffset + (root.taskSwitcherHelpers.notHomeScreenState ? scrollXOffset : 0)) / root.taskSwitcherHelpers.currentScale - (root.homeTouchPositionX * (1 - Math.max(0, Math.min(1, (taskOffsetNormalized - root.taskSwitcherHelpers.peekOffsetValue) / (root.taskSwitcherHelpers.homeOffsetValue - root.taskSwitcherHelpers.peekOffsetValue)))))) * taskOffsetNormalized * isOffScreenOffset * offsetDir // extra resistance calculated for non-current task in the task drawer readonly property real nonCurrentScaleResistance: ((isCurrentTask && root.taskSwitcherHelpers.notHomeScreenState) || root.taskSwitcherHelpers.fromButton) ? 0 : 1 - Math.min(root.taskSwitcherHelpers.currentScale, 1) readonly property real nonCurrentScaleXOffset: (isCurrentTask && root.taskSwitcherHelpers.notHomeScreenState) ? 0 : ((root.taskSwitcherHelpers.taskWidth) * (scale - 1) * (currentIndex - root.taskSwitcherHelpers.currentDisplayTask)) readonly property real nonCurrentXPositionResistance: (isCurrentTask && root.taskSwitcherHelpers.notHomeScreenState) ? 0 : (root.taskSwitcherHelpers.taskWidth * (scale - 1)) * (root.taskSwitcherHelpers.notHomeScreenState ? 0.25 : 1.0) * offsetDir readonly property real nonCurrentYPositionResistance: (isCurrentTask && root.taskSwitcherHelpers.notHomeScreenState) ? 0 : ((taskSwitcher.height / 2)) * nonCurrentScaleResistance // this is the actual displayed x-position on screen x: listX + repeater.leftMargin - root.taskSwitcherState.xPosition - taskOffset - nonCurrentScaleXOffset + nonCurrentXPositionResistance y: ((root.taskSwitcherState.wasInActiveTask || root.taskSwitcherHelpers.taskDrawerOpened) ? root.taskY + nonCurrentYPositionResistance * 0.5: root.taskY / (root.taskSwitcherHelpers.fromButton ? 1 : (1 + taskOffsetNormalized * 0.075))) // add more resistance when not the current task scale: ((isCurrentTask && root.taskSwitcherHelpers.notHomeScreenState) || root.taskSwitcherHelpers.fromButton) ? 1 : (1 + nonCurrentScaleResistance) * (1 + taskOffsetNormalized * 0.075) // add more resistance when not the current task and resist even further if the task is offset // ensure current task is above others z: isCurrentTask ? 1 : 0 // only show header once task switcher is opened showHeader: !root.taskSwitcherState.gestureInProgress && !root.taskSwitcherHelpers.currentlyBeingClosed && !root.taskSwitcherHelpers.isInTaskScrubMode // darken effect as task gets away from the center of the screen darken: { const distFromCentreProgress = Math.abs(x - repeater.leftMargin - (root.taskSwitcherHelpers.currentlyBeingOpened || root.taskSwitcherHelpers.currentlyBeingClosed ? (root.taskSwitcherHelpers.xPositionFromTaskIndex(root.taskSwitcherHelpers.currentDisplayTask)) - root.taskSwitcherState.xPosition : 0)) / root.taskSwitcherHelpers.taskWidth; const upperBoundAdjust = Math.min(0.25, distFromCentreProgress) - 0.2; return Math.max(0, upperBoundAdjust); } // fade out as the task closes opacity: root.taskSwitcherHelpers.closingFactor // update count of tasks being interacted with, so we know whether we are in a swipe up action onInteractingActiveChanged: { let offset = interactingActive ? 1 : -1; root.taskInteractingCount = Math.max(0, root.taskInteractingCount + offset); } width: root.taskSwitcherHelpers.taskWidth height: root.taskSwitcherHelpers.taskHeight previewWidth: root.taskSwitcherHelpers.previewWidth previewHeight: root.taskSwitcherHelpers.previewHeight taskSwitcher: root.taskSwitcher } } }