// SPDX-FileCopyrightText: 2026 Marco Allegretti // SPDX-License-Identifier: EUPL-1.2 /*global effect, effects, animate, animationTime, cancel, Effect, QEasingCurve */ "use strict"; class ShiftTileAnimationsEffect { constructor() { effect.configChanged.connect(this.loadConfig.bind(this)); effect.animationEnded.connect(this.cleanupWindow.bind(this)); effects.windowAdded.connect(this.manage.bind(this)); effects.windowClosed.connect(this.cleanupWindow.bind(this)); this.loadConfig(); for (const window of effects.stackingOrder) { this.manage(window); } } loadConfig() { this.baseDuration = effect.readConfig("Duration", 185) || 185; this.minimumDelta = effect.readConfig("MinimumDelta", 3) || 3; } manage(window) { if (!window || window.shiftTileAnimationsManaged) { return; } window.shiftTileAnimationsManaged = true; window.shiftTileUserMoveResize = false; window.windowFrameGeometryChanged.connect(this.onWindowFrameGeometryChanged.bind(this)); window.windowStartUserMovedResized.connect(this.onWindowStartUserMovedResized.bind(this)); window.windowFinishUserMovedResized.connect(this.onWindowFinishUserMovedResized.bind(this)); } eligibleWindow(window) { return window && !effects.hasActiveFullScreenEffect && window.visible && !window.deleted && window.managed && window.normalWindow && !window.fullScreen && !window.desktopWindow && !window.dock && !window.popup && !window.popupWindow && !window.outline; } validGeometry(geometry) { return geometry && geometry.width > 0 && geometry.height > 0; } movedEnough(oldGeometry, newGeometry) { return Math.abs(oldGeometry.x - newGeometry.x) >= this.minimumDelta || Math.abs(oldGeometry.y - newGeometry.y) >= this.minimumDelta || Math.abs(oldGeometry.width - newGeometry.width) >= this.minimumDelta || Math.abs(oldGeometry.height - newGeometry.height) >= this.minimumDelta; } durationFor(oldGeometry, newGeometry) { const oldCenterX = oldGeometry.x + oldGeometry.width / 2; const oldCenterY = oldGeometry.y + oldGeometry.height / 2; const newCenterX = newGeometry.x + newGeometry.width / 2; const newCenterY = newGeometry.y + newGeometry.height / 2; const distanceX = newCenterX - oldCenterX; const distanceY = newCenterY - oldCenterY; const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); const resize = Math.abs(newGeometry.width - oldGeometry.width) + Math.abs(newGeometry.height - oldGeometry.height); const travelAllowance = Math.min(80, Math.round(Math.max(distance / 12, resize / 18))); return animationTime(Math.max(135, Math.min(260, this.baseDuration + travelAllowance))); } cancelWindowAnimation(window) { if (!window || window.shiftTileMoveAnimation === undefined) { return; } cancel(window.shiftTileMoveAnimation); window.shiftTileMoveAnimation = undefined; window.setData(Effect.WindowForceBlurRole, null); } cleanupWindow(window) { if (!window) { return; } window.shiftTileMoveAnimation = undefined; window.setData(Effect.WindowForceBlurRole, null); } onWindowStartUserMovedResized(window) { if (!window) { return; } window.shiftTileUserMoveResize = true; this.cancelWindowAnimation(window); } onWindowFinishUserMovedResized(window) { if (window) { window.shiftTileUserMoveResize = false; } } onWindowFrameGeometryChanged(window, oldGeometry) { if (!this.eligibleWindow(window) || window.shiftTileUserMoveResize) { return; } const newGeometry = window.geometry; if (!this.validGeometry(oldGeometry) || !this.validGeometry(newGeometry) || !this.movedEnough(oldGeometry, newGeometry)) { return; } this.cancelWindowAnimation(window); window.setData(Effect.WindowForceBlurRole, true); window.shiftTileMoveAnimation = animate({ window: window, duration: this.durationFor(oldGeometry, newGeometry), keepAlive: false, animations: [{ type: Effect.Size, from: { value1: oldGeometry.width, value2: oldGeometry.height }, to: { value1: newGeometry.width, value2: newGeometry.height }, curve: QEasingCurve.OutCubic }, { type: Effect.Translation, from: { value1: oldGeometry.x - newGeometry.x - (newGeometry.width / 2 - oldGeometry.width / 2), value2: oldGeometry.y - newGeometry.y - (newGeometry.height / 2 - oldGeometry.height / 2) }, to: { value1: 0, value2: 0 }, curve: QEasingCurve.OutCubic }] }); } } new ShiftTileAnimationsEffect();