shift-shell/kwin/mobiletaskswitcher/package/contents/ui/TaskList.qml
Marco Allegretti 753909a6ce Move shell and KWin surfaces to shared motion
Apply Motion tokens to startup, applet configuration, lockscreen controls, logout and splash transitions, KWin effects, and the mobile task switcher. Preserve timing-specific behavior where it drives runtime state.
2026-05-21 11:14:42 +02:00

251 lines
13 KiB
QML

// SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2025 Luis Büchi <luis.buechi@kdemail.net>
// 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.plasma.private.mobileshell as MobileShell
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 = root.taskSwitcherHelpers.longAnimationDuration * 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 {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.Standard
duration: root.taskSwitcherHelpers.longAnimationDuration
}
}
// 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 {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.Standard
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
}
}
}