File indexing completed on 2024-11-10 04:57:09

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2007 Lubos Lunak <l.lunak@kde.org>
0006     SPDX-FileCopyrightText: 2008 Lucas Murray <lmurray@undefinedfire.com>
0007     SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 // own
0013 #include "slide.h"
0014 #include "core/output.h"
0015 #include "effect/effecthandler.h"
0016 
0017 // KConfigSkeleton
0018 #include "slideconfig.h"
0019 
0020 #include <cmath>
0021 
0022 namespace KWin
0023 {
0024 
0025 SlideEffect::SlideEffect()
0026 {
0027     SlideConfig::instance(effects->config());
0028     reconfigure(ReconfigureAll);
0029 
0030     connect(effects, &EffectsHandler::desktopChanged,
0031             this, &SlideEffect::desktopChanged);
0032     connect(effects, &EffectsHandler::desktopChanging,
0033             this, &SlideEffect::desktopChanging);
0034     connect(effects, QOverload<>::of(&EffectsHandler::desktopChangingCancelled),
0035             this, &SlideEffect::desktopChangingCancelled);
0036     connect(effects, &EffectsHandler::windowAdded,
0037             this, &SlideEffect::windowAdded);
0038     connect(effects, &EffectsHandler::windowDeleted,
0039             this, &SlideEffect::windowDeleted);
0040     connect(effects, &EffectsHandler::desktopAdded,
0041             this, &SlideEffect::finishedSwitching);
0042     connect(effects, &EffectsHandler::desktopRemoved,
0043             this, &SlideEffect::finishedSwitching);
0044     connect(effects, &EffectsHandler::screenAdded,
0045             this, &SlideEffect::finishedSwitching);
0046     connect(effects, &EffectsHandler::screenRemoved,
0047             this, &SlideEffect::finishedSwitching);
0048 
0049     m_currentPosition = effects->desktopGridCoords(effects->currentDesktop());
0050 }
0051 
0052 SlideEffect::~SlideEffect()
0053 {
0054     finishedSwitching();
0055 }
0056 
0057 bool SlideEffect::supported()
0058 {
0059     return effects->animationsSupported();
0060 }
0061 
0062 void SlideEffect::reconfigure(ReconfigureFlags)
0063 {
0064     SlideConfig::self()->read();
0065 
0066     const qreal springConstant = 300.0 / effects->animationTimeFactor();
0067     const qreal dampingRatio = 1.1;
0068 
0069     m_motionX = SpringMotion(springConstant, dampingRatio);
0070     m_motionY = SpringMotion(springConstant, dampingRatio);
0071 
0072     m_hGap = SlideConfig::horizontalGap();
0073     m_vGap = SlideConfig::verticalGap();
0074     m_slideBackground = SlideConfig::slideBackground();
0075 }
0076 
0077 inline QRegion buildClipRegion(const QPoint &pos, int w, int h)
0078 {
0079     const QSize screenSize = effects->virtualScreenSize();
0080     QRegion r = QRect(pos, screenSize);
0081     if (effects->optionRollOverDesktops()) {
0082         r += (r & QRect(-w, 0, w, h)).translated(w, 0); // W
0083         r += (r & QRect(w, 0, w, h)).translated(-w, 0); // E
0084 
0085         r += (r & QRect(0, -h, w, h)).translated(0, h); // N
0086         r += (r & QRect(0, h, w, h)).translated(0, -h); // S
0087 
0088         r += (r & QRect(-w, -h, w, h)).translated(w, h); // NW
0089         r += (r & QRect(w, -h, w, h)).translated(-w, h); // NE
0090         r += (r & QRect(w, h, w, h)).translated(-w, -h); // SE
0091         r += (r & QRect(-w, h, w, h)).translated(w, -h); // SW
0092     }
0093     return r;
0094 }
0095 
0096 void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
0097 {
0098     std::chrono::milliseconds timeDelta = std::chrono::milliseconds::zero();
0099     if (m_lastPresentTime.count()) {
0100         timeDelta = presentTime - m_lastPresentTime;
0101     }
0102     m_lastPresentTime = presentTime;
0103 
0104     if (m_state == State::ActiveAnimation) {
0105         m_motionX.advance(timeDelta);
0106         m_motionY.advance(timeDelta);
0107         const QSize virtualSpaceSize = effects->virtualScreenSize();
0108         m_currentPosition.setX(m_motionX.position() / virtualSpaceSize.width());
0109         m_currentPosition.setY(m_motionY.position() / virtualSpaceSize.height());
0110     }
0111 
0112     const QList<VirtualDesktop *> desktops = effects->desktops();
0113     const int w = effects->desktopGridWidth();
0114     const int h = effects->desktopGridHeight();
0115 
0116     // Clipping
0117     m_paintCtx.visibleDesktops.clear();
0118     m_paintCtx.visibleDesktops.reserve(4); // 4 - maximum number of visible desktops
0119     bool includedX = false, includedY = false;
0120     for (VirtualDesktop *desktop : desktops) {
0121         const QPoint coords = effects->desktopGridCoords(desktop);
0122         if (coords.x() % w == (int)(m_currentPosition.x()) % w) {
0123             includedX = true;
0124         } else if (coords.x() % w == ((int)(m_currentPosition.x()) + 1) % w) {
0125             includedX = true;
0126         }
0127         if (coords.y() % h == (int)(m_currentPosition.y()) % h) {
0128             includedY = true;
0129         } else if (coords.y() % h == ((int)(m_currentPosition.y()) + 1) % h) {
0130             includedY = true;
0131         }
0132 
0133         if (includedX && includedY) {
0134             m_paintCtx.visibleDesktops << desktop;
0135         }
0136     }
0137 
0138     data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST;
0139 
0140     effects->prePaintScreen(data, presentTime);
0141 }
0142 
0143 void SlideEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &region, Output *screen)
0144 {
0145     m_paintCtx.wrap = effects->optionRollOverDesktops();
0146     effects->paintScreen(renderTarget, viewport, mask, region, screen);
0147 }
0148 
0149 QPoint SlideEffect::getDrawCoords(QPointF pos, Output *screen)
0150 {
0151     QPoint c = QPoint();
0152     c.setX(pos.x() * (screen->geometry().width() + m_hGap));
0153     c.setY(pos.y() * (screen->geometry().height() + m_vGap));
0154     return c;
0155 }
0156 
0157 /**
0158  * Decide whether given window @p w should be transformed/translated.
0159  * @returns @c true if given window @p w should be transformed, otherwise @c false
0160  */
0161 bool SlideEffect::isTranslated(const EffectWindow *w) const
0162 {
0163     if (w->isOnAllDesktops()) {
0164         if (w->isDesktop()) {
0165             return m_slideBackground;
0166         }
0167         return false;
0168     } else if (w == m_movingWindow) {
0169         return false;
0170     }
0171     return true;
0172 }
0173 
0174 /**
0175  * Will a window be painted during this frame?
0176  */
0177 bool SlideEffect::willBePainted(const EffectWindow *w) const
0178 {
0179     if (w->isOnAllDesktops()) {
0180         return true;
0181     }
0182     if (w == m_movingWindow) {
0183         return true;
0184     }
0185     for (VirtualDesktop *desktop : std::as_const(m_paintCtx.visibleDesktops)) {
0186         if (w->isOnDesktop(desktop)) {
0187             return true;
0188         }
0189     }
0190     return false;
0191 }
0192 
0193 void SlideEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime)
0194 {
0195     data.setTransformed();
0196     effects->prePaintWindow(w, data, presentTime);
0197 }
0198 
0199 void SlideEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, QRegion region, WindowPaintData &data)
0200 {
0201     if (!willBePainted(w)) {
0202         return;
0203     }
0204 
0205     if (!isTranslated(w)) {
0206         effects->paintWindow(renderTarget, viewport, w, mask, region, data);
0207         return;
0208     }
0209 
0210     const int gridWidth = effects->desktopGridWidth();
0211     const int gridHeight = effects->desktopGridHeight();
0212 
0213     QPointF drawPosition = forcePositivePosition(m_currentPosition);
0214     drawPosition = m_paintCtx.wrap ? constrainToDrawableRange(drawPosition) : drawPosition;
0215 
0216     // If we're wrapping, draw the desktop in the second position.
0217     const bool wrappingX = drawPosition.x() > gridWidth - 1;
0218     const bool wrappingY = drawPosition.y() > gridHeight - 1;
0219 
0220     const auto screens = effects->screens();
0221 
0222     for (VirtualDesktop *desktop : std::as_const(m_paintCtx.visibleDesktops)) {
0223         if (!w->isOnDesktop(desktop)) {
0224             continue;
0225         }
0226         QPointF desktopTranslation = QPointF(effects->desktopGridCoords(desktop)) - drawPosition;
0227         // Decide if that first desktop should be drawn at 0 or the higher position used for wrapping.
0228         if (effects->desktopGridCoords(desktop).x() == 0 && wrappingX) {
0229             desktopTranslation = QPointF(desktopTranslation.x() + gridWidth, desktopTranslation.y());
0230         }
0231         if (effects->desktopGridCoords(desktop).y() == 0 && wrappingY) {
0232             desktopTranslation = QPointF(desktopTranslation.x(), desktopTranslation.y() + gridHeight);
0233         }
0234 
0235         for (Output *screen : screens) {
0236             QPoint drawTranslation = getDrawCoords(desktopTranslation, screen);
0237             data += drawTranslation;
0238 
0239             const QRect screenArea = screen->geometry();
0240             const QRect damage = screenArea.translated(drawTranslation).intersected(screenArea);
0241 
0242             effects->paintWindow(
0243                 renderTarget, viewport, w, mask,
0244                 // Only paint the region that intersects the current screen and desktop.
0245                 region.intersected(damage),
0246                 data);
0247 
0248             // Undo the translation for the next screen. I know, it hurts me too.
0249             data += QPoint(-drawTranslation.x(), -drawTranslation.y());
0250         }
0251     }
0252 }
0253 
0254 void SlideEffect::postPaintScreen()
0255 {
0256     if (m_state == State::ActiveAnimation && !m_motionX.isMoving() && !m_motionY.isMoving()) {
0257         finishedSwitching();
0258     }
0259 
0260     effects->addRepaintFull();
0261     effects->postPaintScreen();
0262 }
0263 
0264 /*
0265  * Negative desktop positions aren't allowed.
0266  */
0267 QPointF SlideEffect::forcePositivePosition(QPointF p) const
0268 {
0269     if (p.x() < 0) {
0270         p.setX(p.x() + std::ceil(-p.x() / effects->desktopGridWidth()) * effects->desktopGridWidth());
0271     }
0272     if (p.y() < 0) {
0273         p.setY(p.y() + std::ceil(-p.y() / effects->desktopGridHeight()) * effects->desktopGridHeight());
0274     }
0275     return p;
0276 }
0277 
0278 bool SlideEffect::shouldElevate(const EffectWindow *w) const
0279 {
0280     // Static docks(i.e. this effect doesn't slide docks) should be elevated
0281     // so they can properly animate themselves when an user enters or leaves
0282     // a virtual desktop with a window in fullscreen mode.
0283     return w->isDock();
0284 }
0285 
0286 /*
0287  * This function is called when the desktop changes.
0288  * Called AFTER the gesture is released.
0289  * Sets up animation to round off to the new current desktop.
0290  */
0291 void SlideEffect::startAnimation(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *movingWindow)
0292 {
0293     if (m_state == State::Inactive) {
0294         prepareSwitching();
0295     }
0296 
0297     m_state = State::ActiveAnimation;
0298     m_movingWindow = movingWindow;
0299 
0300     m_startPos = m_currentPosition;
0301     m_endPos = effects->desktopGridCoords(current);
0302     if (effects->optionRollOverDesktops()) {
0303         optimizePath();
0304     }
0305 
0306     const QSize virtualSpaceSize = effects->virtualScreenSize();
0307     m_motionX.setAnchor(m_endPos.x() * virtualSpaceSize.width());
0308     m_motionX.setPosition(m_startPos.x() * virtualSpaceSize.width());
0309     m_motionY.setAnchor(m_endPos.y() * virtualSpaceSize.height());
0310     m_motionY.setPosition(m_startPos.y() * virtualSpaceSize.height());
0311 
0312     effects->setActiveFullScreenEffect(this);
0313     effects->addRepaintFull();
0314 }
0315 
0316 void SlideEffect::prepareSwitching()
0317 {
0318     const auto windows = effects->stackingOrder();
0319     m_windowData.reserve(windows.count());
0320 
0321     for (EffectWindow *w : windows) {
0322         m_windowData[w] = WindowData{
0323             .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP),
0324         };
0325 
0326         if (shouldElevate(w)) {
0327             effects->setElevatedWindow(w, true);
0328             m_elevatedWindows << w;
0329         }
0330         w->setData(WindowForceBackgroundContrastRole, QVariant(true));
0331         w->setData(WindowForceBlurRole, QVariant(true));
0332     }
0333 }
0334 
0335 void SlideEffect::finishedSwitching()
0336 {
0337     if (m_state == State::Inactive) {
0338         return;
0339     }
0340     const QList<EffectWindow *> windows = effects->stackingOrder();
0341     for (EffectWindow *w : windows) {
0342         w->setData(WindowForceBackgroundContrastRole, QVariant());
0343         w->setData(WindowForceBlurRole, QVariant());
0344     }
0345 
0346     for (EffectWindow *w : std::as_const(m_elevatedWindows)) {
0347         effects->setElevatedWindow(w, false);
0348     }
0349     m_elevatedWindows.clear();
0350 
0351     m_windowData.clear();
0352     m_movingWindow = nullptr;
0353     m_state = State::Inactive;
0354     m_lastPresentTime = std::chrono::milliseconds::zero();
0355     effects->setActiveFullScreenEffect(nullptr);
0356     m_currentPosition = effects->desktopGridCoords(effects->currentDesktop());
0357 }
0358 
0359 void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with)
0360 {
0361     if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) {
0362         m_currentPosition = effects->desktopGridCoords(effects->currentDesktop());
0363         return;
0364     }
0365 
0366     startAnimation(old, current, with);
0367 }
0368 
0369 void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with)
0370 {
0371     if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) {
0372         return;
0373     }
0374 
0375     if (m_state == State::Inactive) {
0376         prepareSwitching();
0377     }
0378 
0379     m_state = State::ActiveGesture;
0380     m_movingWindow = with;
0381 
0382     // Find desktop position based on animationDelta
0383     QPoint gridPos = effects->desktopGridCoords(old);
0384     m_currentPosition.setX(gridPos.x() + desktopOffset.x());
0385     m_currentPosition.setY(gridPos.y() + desktopOffset.y());
0386 
0387     if (effects->optionRollOverDesktops()) {
0388         m_currentPosition = forcePositivePosition(m_currentPosition);
0389     } else {
0390         m_currentPosition = moveInsideDesktopGrid(m_currentPosition);
0391     }
0392 
0393     effects->setActiveFullScreenEffect(this);
0394     effects->addRepaintFull();
0395 }
0396 
0397 void SlideEffect::desktopChangingCancelled()
0398 {
0399     // If the fingers have been lifted and the current desktop didn't change, start animation
0400     // to move back to the original virtual desktop.
0401     if (effects->activeFullScreenEffect() == this) {
0402         startAnimation(effects->currentDesktop(), effects->currentDesktop(), nullptr);
0403     }
0404 }
0405 
0406 QPointF SlideEffect::moveInsideDesktopGrid(QPointF p)
0407 {
0408     if (p.x() < 0) {
0409         p.setX(0);
0410     }
0411     if (p.y() < 0) {
0412         p.setY(0);
0413     }
0414     if (p.x() > effects->desktopGridWidth() - 1) {
0415         p.setX(effects->desktopGridWidth() - 1);
0416     }
0417     if (p.y() > effects->desktopGridHeight() - 1) {
0418         p.setY(effects->desktopGridHeight() - 1);
0419     }
0420     return p;
0421 }
0422 
0423 void SlideEffect::windowAdded(EffectWindow *w)
0424 {
0425     if (m_state == State::Inactive) {
0426         return;
0427     }
0428     if (shouldElevate(w)) {
0429         effects->setElevatedWindow(w, true);
0430         m_elevatedWindows << w;
0431     }
0432     w->setData(WindowForceBackgroundContrastRole, QVariant(true));
0433     w->setData(WindowForceBlurRole, QVariant(true));
0434 
0435     m_windowData[w] = WindowData{
0436         .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP),
0437     };
0438 }
0439 
0440 void SlideEffect::windowDeleted(EffectWindow *w)
0441 {
0442     if (m_state == State::Inactive) {
0443         return;
0444     }
0445     if (w == m_movingWindow) {
0446         m_movingWindow = nullptr;
0447     }
0448     m_elevatedWindows.removeAll(w);
0449     m_windowData.remove(w);
0450 }
0451 
0452 /*
0453  * Find the fastest path between two desktops.
0454  * This function decides when it's better to wrap around the grid or not.
0455  * Only call if wrapping is enabled.
0456  */
0457 void SlideEffect::optimizePath()
0458 {
0459     int w = effects->desktopGridWidth();
0460     int h = effects->desktopGridHeight();
0461 
0462     // Keep coordinates as low as possible
0463     if (m_startPos.x() >= w && m_endPos.x() >= w) {
0464         m_startPos.setX(fmod(m_startPos.x(), w));
0465         m_endPos.setX(fmod(m_endPos.x(), w));
0466     }
0467     if (m_startPos.y() >= h && m_endPos.y() >= h) {
0468         m_startPos.setY(fmod(m_startPos.y(), h));
0469         m_endPos.setY(fmod(m_endPos.y(), h));
0470     }
0471 
0472     // Is there is a shorter possible route?
0473     // If the x distance to be traveled is more than half the grid width, it's faster to wrap.
0474     // To avoid negative coordinates, take the lower coordinate and raise.
0475     if (std::abs((m_startPos.x() - m_endPos.x())) > w / 2.0) {
0476         if (m_startPos.x() < m_endPos.x()) {
0477             while (m_startPos.x() < m_endPos.x()) {
0478                 m_startPos.setX(m_startPos.x() + w);
0479             }
0480         } else {
0481             while (m_endPos.x() < m_startPos.x()) {
0482                 m_endPos.setX(m_endPos.x() + w);
0483             }
0484         }
0485         // Keep coordinates as low as possible
0486         if (m_startPos.x() >= w && m_endPos.x() >= w) {
0487             m_startPos.setX(fmod(m_startPos.x(), w));
0488             m_endPos.setX(fmod(m_endPos.x(), w));
0489         }
0490     }
0491 
0492     // Same for y
0493     if (std::abs((m_endPos.y() - m_startPos.y())) > (double)h / (double)2) {
0494         if (m_startPos.y() < m_endPos.y()) {
0495             while (m_startPos.y() < m_endPos.y()) {
0496                 m_startPos.setY(m_startPos.y() + h);
0497             }
0498         } else {
0499             while (m_endPos.y() < m_startPos.y()) {
0500                 m_endPos.setY(m_endPos.y() + h);
0501             }
0502         }
0503         // Keep coordinates as low as possible
0504         if (m_startPos.y() >= h && m_endPos.y() >= h) {
0505             m_startPos.setY(fmod(m_startPos.y(), h));
0506             m_endPos.setY(fmod(m_endPos.y(), h));
0507         }
0508     }
0509 }
0510 
0511 /*
0512  * Takes the point and uses modulus to keep draw position within [0, desktopGridWidth]
0513  * The render loop will draw the first desktop (0) after the last one (at position desktopGridWidth) for the wrap animation.
0514  * This function finds the true fastest path, regardless of which direction the animation is already going;
0515  * I was a little upset about this limitation until I realized that MacOS can't even wrap desktops :)
0516  */
0517 QPointF SlideEffect::constrainToDrawableRange(QPointF p)
0518 {
0519     p.setX(fmod(p.x(), effects->desktopGridWidth()));
0520     p.setY(fmod(p.y(), effects->desktopGridHeight()));
0521     return p;
0522 }
0523 
0524 } // namespace KWin
0525 
0526 #include "moc_slide.cpp"