From 003c820e0047337aaf714ea765f15b2b1e9a9d26 Mon Sep 17 00:00:00 2001 From: Eric Edlund Date: Tue, 22 Mar 2022 18:48:43 -0400 Subject: [PATCH] rework of slide effect internals Fixed a bunch of bugs and polished the slide effect. Plugged the slide effect into the new VirtualDesktopManager interface desktopChanging() to allow for mac os style desktop switching. BUG: 448419 BUG: 401479 --- src/effects/slide/slide.cpp | 396 ++++++++++++++++++++++++------------ src/effects/slide/slide.h | 56 ++++- src/virtualdesktops.cpp | 2 +- 3 files changed, 316 insertions(+), 138 deletions(-) diff --git a/src/effects/slide/slide.cpp b/src/effects/slide/slide.cpp index 07497b4293..a477240807 100644 --- a/src/effects/slide/slide.cpp +++ b/src/effects/slide/slide.cpp @@ -15,6 +15,8 @@ // KConfigSkeleton #include "slideconfig.h" +#include + namespace KWin { @@ -27,21 +29,27 @@ SlideEffect::SlideEffect() connect(effects, QOverload::of(&EffectsHandler::desktopChanged), this, &SlideEffect::desktopChanged); + connect(effects, QOverload::of(&EffectsHandler::desktopChanging), + this, &SlideEffect::desktopChanging); + connect(effects, QOverload<>::of(&EffectsHandler::desktopChangingCancelled), + this, &SlideEffect::desktopChangingCancelled); connect(effects, &EffectsHandler::windowAdded, this, &SlideEffect::windowAdded); connect(effects, &EffectsHandler::windowDeleted, this, &SlideEffect::windowDeleted); connect(effects, &EffectsHandler::numberDesktopsChanged, - this, &SlideEffect::stop); + this, &SlideEffect::finishedSwitching); connect(effects, &EffectsHandler::screenAdded, - this, &SlideEffect::stop); + this, &SlideEffect::finishedSwitching); connect(effects, &EffectsHandler::screenRemoved, - this, &SlideEffect::stop); + this, &SlideEffect::finishedSwitching); + + m_currentPosition = effects->desktopCoords(effects->currentDesktop()); } SlideEffect::~SlideEffect() { - stop(); + finishedSwitching(); } bool SlideEffect::supported() @@ -53,8 +61,8 @@ void SlideEffect::reconfigure(ReconfigureFlags) { SlideConfig::self()->read(); - m_timeLine.setDuration( - std::chrono::milliseconds(animationTime(500))); + m_fullAnimationDuration = animationTime(500); + m_timeLine.setDuration(std::chrono::milliseconds(m_fullAnimationDuration)); m_hGap = SlideConfig::horizontalGap(); m_vGap = SlideConfig::verticalGap(); @@ -83,33 +91,39 @@ inline QRegion buildClipRegion(const QPoint &pos, int w, int h) void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) { - std::chrono::milliseconds delta = std::chrono::milliseconds::zero(); + std::chrono::milliseconds timeDelta = std::chrono::milliseconds::zero(); if (m_lastPresentTime.count()) { - delta = presentTime - m_lastPresentTime; + timeDelta = presentTime - m_lastPresentTime; } m_lastPresentTime = presentTime; - m_timeLine.update(delta); + m_timeLine.update(timeDelta); - const int w = workspaceWidth(); - const int h = workspaceHeight(); - - // When "Desktop navigation wraps around" checkbox is checked, currentPos - // can be outside the rectangle Rect{x:-w, y:-h, width:2*w, height: 2*h}, - // so we map currentPos back to the rect. - m_paintCtx.currentPos = m_startPos + m_diff * m_timeLine.value(); - if (effects->optionRollOverDesktops()) { - m_paintCtx.currentPos.setX(m_paintCtx.currentPos.x() % w); - m_paintCtx.currentPos.setY(m_paintCtx.currentPos.y() % h); + if (!m_gestureActive) { // When animating + m_currentPosition = m_startPos + (m_endPos - m_startPos) * m_timeLine.value(); } + const int w = effects->desktopGridWidth(); + const int h = effects->desktopGridHeight(); + + //Clipping m_paintCtx.visibleDesktops.clear(); - const QRegion clipRegion = buildClipRegion(m_paintCtx.currentPos, w, h); + m_paintCtx.visibleDesktops.reserve(4); // 4 - maximum number of visible desktops + bool includedX = false, includedY = false; for (int i = 1; i <= effects->numberOfDesktops(); i++) { - const QRect desktopGeo = desktopGeometry(i); - if (!clipRegion.contains(desktopGeo)) { - continue; + if (effects->desktopGridCoords(i).x() % w == (int)(m_currentPosition.x()) % w) { + includedX = true; + } else if (effects->desktopGridCoords(i).x() % w == ((int)(m_currentPosition.x()) + 1) % w) { + includedX = true; + } + if (effects->desktopGridCoords(i).y() % h == (int)(m_currentPosition.y()) % h) { + includedY = true; + } else if (effects->desktopGridCoords(i).y() % h == ((int)(m_currentPosition.y()) + 1) % h) { + includedY = true; + } + + if (includedX && includedY) { + m_paintCtx.visibleDesktops << i; } - m_paintCtx.visibleDesktops << i; } data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST; @@ -117,37 +131,28 @@ void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::millisec effects->prePaintScreen(data, presentTime); } -/** - * Wrap vector @p diff around grid @p w x @p h. - * - * Wrapping is done in such a way that magnitude of x and y component of vector - * @p diff is less than half of @p w and half of @p h, respectively. This will - * result in having the "shortest" path between two points. - * - * @param diff Vector between two points - * @param w Width of the desktop grid - * @param h Height of the desktop grid - */ -inline void wrapDiff(QPoint &diff, int w, int h) -{ - if (diff.x() > w / 2) { - diff.setX(diff.x() - w); - } else if (diff.x() < -w / 2) { - diff.setX(diff.x() + w); - } - - if (diff.y() > h / 2) { - diff.setY(diff.y() - h); - } else if (diff.y() < -h / 2) { - diff.setY(diff.y() + h); - } -} - void SlideEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData &data) { const bool wrap = effects->optionRollOverDesktops(); - const int w = workspaceWidth(); - const int h = workspaceHeight(); + const int w = effects->desktopGridWidth(); + const int h = effects->desktopGridHeight(); + bool wrappingX = false, wrappingY = false; + + QPointF drawPosition = forcePositivePosition(m_currentPosition); + + if (wrap) { + drawPosition = constrainToDrawableRange(drawPosition); + } + + //If we're wrapping, draw the desktop in the second position. + if (drawPosition.x() > w - 1) { + wrappingX = true; + } + + if (drawPosition.y() > h - 1) { + wrappingY = true; + } + // When we enter a virtual desktop that has a window in fullscreen mode, // stacking order is fine. When we leave a virtual desktop that has @@ -176,15 +181,30 @@ void SlideEffect::paintScreen(int mask, const QRegion ®ion, ScreenPaintData & for (int desktop : qAsConst(m_paintCtx.visibleDesktops)) { m_paintCtx.desktop = desktop; m_paintCtx.lastPass = (lastDesktop == desktop); - m_paintCtx.translation = desktopCoords(desktop) - m_paintCtx.currentPos; - if (wrap) { - wrapDiff(m_paintCtx.translation, w, h); + m_paintCtx.translation = QPointF(effects->desktopGridCoords(desktop)) - drawPosition;//TODO: verify + + // Decide if that first desktop should be drawn at 0 or the higher position used for wrapping. + if (effects->desktopGridCoords(desktop).x() == 0 && wrappingX) { + m_paintCtx.translation = QPointF(m_paintCtx.translation.x() + w, m_paintCtx.translation.y()); } + + if (effects->desktopGridCoords(desktop).y() == 0 && wrappingY) { + m_paintCtx.translation = QPointF(m_paintCtx.translation.x(), m_paintCtx.translation.y() + h); + } + effects->paintScreen(mask, region, data); m_paintCtx.firstPass = false; } } +QPoint SlideEffect::getDrawCoords(QPointF pos, EffectScreen *screen) +{ + QPoint c = QPoint(); + c.setX(pos.x() * (screen->geometry().width() + m_hGap)); + c.setY(pos.y() * (screen->geometry().height() + m_vGap)); + return c; +} + /** * Decide whether given window @p w should be transformed/translated. * @returns @c true if given window @p w should be transformed, otherwise @c false @@ -264,64 +284,49 @@ void SlideEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowP if (!isPainted(w)) { return; } - if (isTranslated(w)) { - data += m_paintCtx.translation; + + for (EffectScreen *screen: effects->screens()) { + QPoint translation = getDrawCoords(m_paintCtx.translation, screen); + if (isTranslated(w)) { + data += translation; + } + + effects->paintWindow( + w, + mask, + // Only paint the region that intersects the current screen and desktop. + region.intersected(effects->clientArea(ScreenArea, w)).intersected(effects->clientArea(ScreenArea, screen, effects->currentDesktop())), + data); + + if (isTranslated(w)) { + // Undo the translation for the next screen. I know, it hurts me too. + data += QPoint(-translation.x(), -translation.y()); + } } - effects->paintWindow(w, mask, region, data); } void SlideEffect::postPaintScreen() { - if (m_timeLine.done()) { - stop(); + if (m_timeLine.done() && !m_gestureActive) { + finishedSwitching(); } effects->addRepaintFull(); effects->postPaintScreen(); } -/** - * Get position of the top-left corner of desktop @p id within desktop grid with gaps. - * @param id ID of a virtual desktop +/* + * Negative desktop positions aren't allowed. */ -QPoint SlideEffect::desktopCoords(int id) const +QPointF SlideEffect::forcePositivePosition(QPointF p) const { - QPoint c = effects->desktopCoords(id); - const QPoint gridPos = effects->desktopGridCoords(id); - c.setX(c.x() + m_hGap * gridPos.x()); - c.setY(c.y() + m_vGap * gridPos.y()); - return c; -} - -/** - * Get geometry of desktop @p id within desktop grid with gaps. - * @param id ID of a virtual desktop - */ -QRect SlideEffect::desktopGeometry(int id) const -{ - QRect g = effects->virtualScreenGeometry(); - g.translate(desktopCoords(id)); - return g; -} - -/** - * Get width of a virtual desktop grid. - */ -int SlideEffect::workspaceWidth() const -{ - int w = effects->workspaceWidth(); - w += m_hGap * effects->desktopGridWidth(); - return w; -} - -/** - * Get height of a virtual desktop grid. - */ -int SlideEffect::workspaceHeight() const -{ - int h = effects->workspaceHeight(); - h += m_vGap * effects->desktopGridHeight(); - return h; + if (p.x() < 0) { + p.setX(p.x() + std::ceil(-p.x() / effects->desktopGridWidth()) * effects->desktopGridWidth()); + } + if (p.y() < 0) { + p.setY(p.y() + std::ceil(-p.y() / effects->desktopGridHeight()) * effects->desktopGridHeight()); + } + return p; } bool SlideEffect::shouldElevate(const EffectWindow *w) const @@ -332,28 +337,20 @@ bool SlideEffect::shouldElevate(const EffectWindow *w) const return w->isDock() && !m_slideDocks; } -void SlideEffect::start(int old, int current, EffectWindow *movingWindow) +/* + * This function is called when the desktop changes. + * Called AFTER the gesture is released. + * Sets up animation to round off to the new current desktop. + */ +void SlideEffect::startAnimation(int old, int current, EffectWindow *movingWindow) { + Q_UNUSED(old) + m_movingWindow = movingWindow; const bool wrap = effects->optionRollOverDesktops(); - const int w = workspaceWidth(); - const int h = workspaceHeight(); - - if (m_active) { - QPoint passed = m_diff * m_timeLine.value(); - QPoint currentPos = m_startPos + passed; - QPoint delta = desktopCoords(current) - desktopCoords(old); - if (wrap) { - wrapDiff(delta, w, h); - } - m_diff += delta - passed; - m_startPos = currentPos; - // TODO: Figure out how to smooth movement. - m_timeLine.reset(); - return; - } + //Handle stacking order const auto windows = effects->stackingOrder(); for (EffectWindow *w : windows) { if (shouldElevate(w)) { @@ -364,18 +361,35 @@ void SlideEffect::start(int old, int current, EffectWindow *movingWindow) w->setData(WindowForceBlurRole, QVariant(true)); } - m_diff = desktopCoords(current) - desktopCoords(old); - if (wrap) { - wrapDiff(m_diff, w, h); - } - m_startPos = desktopCoords(old); - m_timeLine.reset(); + // Set up animation m_active = true; + m_timeLine.reset(); + + m_startPos = m_currentPosition; + m_endPos = effects->desktopGridCoords(current); + if (wrap) { + optimizePath(); + } + + // Find an apropriate duration + QPointF distance = m_startPos - m_endPos; + distance.setX(std::abs(distance.x())); + distance.setY(std::abs(distance.y())); + if (distance.x() < 1 && distance.y() < 1) { + if (distance.x() > distance.y()) { + m_timeLine.setDuration(std::chrono::milliseconds(std::max(1, (int)(m_fullAnimationDuration * distance.x())))); + } else { + m_timeLine.setDuration(std::chrono::milliseconds(std::max(1, (int)(m_fullAnimationDuration * distance.y())))); + } + } else { + m_timeLine.setDuration(std::chrono::milliseconds(m_fullAnimationDuration)); + } + effects->setActiveFullScreenEffect(this); effects->addRepaintFull(); } -void SlideEffect::stop() +void SlideEffect::finishedSwitching() { if (!m_active) { return; @@ -396,14 +410,72 @@ void SlideEffect::stop() m_active = false; m_lastPresentTime = std::chrono::milliseconds::zero(); effects->setActiveFullScreenEffect(nullptr); + m_currentPosition = effects->desktopGridCoords(effects->currentDesktop()); } void SlideEffect::desktopChanged(int old, int current, EffectWindow *with) { - if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) { + if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) { + m_currentPosition = effects->desktopGridCoords(effects->currentDesktop()); return; } - start(old, current, with); + + m_gestureActive = false; + startAnimation(old, current, with); +} + +void SlideEffect::desktopChanging(uint old, QPointF desktopOffset, EffectWindow *with) +{ + if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) { + return; + } + + m_gestureActive = true; + m_movingWindow = with; + + const bool wrap = effects->optionRollOverDesktops(); + + // Find desktop position based on animationDelta + QPoint gridPos = effects->desktopGridCoords(old); + m_currentPosition.setX(gridPos.x() + desktopOffset.x()); + m_currentPosition.setY(gridPos.y() + desktopOffset.y()); + + if (wrap) { + m_currentPosition = forcePositivePosition(m_currentPosition); + } else { + m_currentPosition = moveInsideDesktopGrid(m_currentPosition); + } + + m_active = true; + effects->setActiveFullScreenEffect(this); + effects->addRepaintFull(); +} + +void SlideEffect::desktopChangingCancelled() +{ + if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) { + return; + } + + m_gestureActive = false; + startAnimation(effects->currentDesktop(), effects->currentDesktop(), nullptr); +} + +QPointF SlideEffect::moveInsideDesktopGrid(QPointF p) +{ + if (p.x() < 0) { + p.setX(0); + } + if (p.y() < 0) { + p.setY(0); + } + if (p.x() > effects->desktopGridWidth() - 1) { + p.setX(effects->desktopGridWidth() - 1); + } + if (p.y() > effects->desktopGridHeight() - 1) { + p.setY(effects->desktopGridHeight() - 1); + } + return p; } void SlideEffect::windowAdded(EffectWindow *w) @@ -431,4 +503,76 @@ void SlideEffect::windowDeleted(EffectWindow *w) m_paintCtx.fullscreenWindows.removeAll(w); } +/* + * Find the fastest path between two desktops. + * This function decides when it's better to wrap around the grid or not. + * Only call if wrapping is enabled. + */ +void SlideEffect::optimizePath() +{ + int w = effects->desktopGridWidth(); + int h = effects->desktopGridHeight(); + + // Keep coordinates as low as possible + if (m_startPos.x() >= w && m_endPos.x() >= w) { + m_startPos.setX(fmod(m_startPos.x(), w)); + m_endPos.setX(fmod(m_endPos.x(), w)); + } + if (m_startPos.y() >= h && m_endPos.y() >= h) { + m_startPos.setY(fmod(m_startPos.y(), h)); + m_endPos.setY(fmod(m_endPos.y(), h)); + } + + // Is there is a shorter possible route? + // If the x distance to be traveled is more than half the grid width, it's faster to wrap. + // To avoid negative coordinates, take the lower coordinate and raise. + if (std::abs((m_startPos.x() - m_endPos.x())) > w / 2.0) { + if (m_startPos.x() < m_endPos.x()) { + while (m_startPos.x() < m_endPos.x()) { + m_startPos.setX(m_startPos.x() + w); + } + } else { + while (m_endPos.x() < m_startPos.x()) { + m_endPos.setX(m_endPos.x() + w); + } + } + // Keep coordinates as low as possible + if (m_startPos.x() >= w && m_endPos.x() >= w) { + m_startPos.setX(fmod(m_startPos.x(), w)); + m_endPos.setX(fmod(m_endPos.x(), w)); + } + } + + // Same for y + if (std::abs((m_endPos.y() - m_startPos.y())) > (double)h / (double)2) { + if (m_startPos.y() < m_endPos.y()) { + while (m_startPos.y() < m_endPos.y()) { + m_startPos.setY(m_startPos.y() + h); + } + } else { + while (m_endPos.y() < m_startPos.y()) { + m_endPos.setY(m_endPos.y() + h); + } + } + // Keep coordinates as low as possible + if (m_startPos.y() >= h && m_endPos.y() >= h) { + m_startPos.setY(fmod(m_startPos.y(), h)); + m_endPos.setY(fmod(m_endPos.y(), h)); + } + } +} + +/* + * Takes the point and uses modulus to keep draw position within [0, desktopGridWidth] + * The render loop will draw the first desktop (0) after the last one (at position desktopGridWidth) for the wrap animation. + * This function finds the true fastest path, regardless of which direction the animation is already going; + * I was a little upset about this limitation until I realized that MacOS can't even wrap desktops :) + */ +QPointF SlideEffect::constrainToDrawableRange(QPointF p) +{ + p.setX(fmod(p.x(), effects->desktopGridWidth())); + p.setY(fmod(p.y(), effects->desktopGridHeight())); + return p; +} + } // namespace KWin diff --git a/src/effects/slide/slide.h b/src/effects/slide/slide.h index d3c6317ade..49c0b0b483 100644 --- a/src/effects/slide/slide.h +++ b/src/effects/slide/slide.h @@ -18,6 +18,31 @@ namespace KWin { +/* + * How it Works: + * + * This effect doesn't change the current desktop, only recieves changes from the VirtualDesktopManager. + * The only visually aparent inputs are desktopChanged() and desktopChanging(). + * + * When responding to desktopChanging(), the draw position is only affected by what's recieved from there. + * After desktopChanging() is done, or without desktopChanging() having been called at all, desktopChanged() is called. + * The desktopChanged() function configures the m_startPos and m_endPos for the animation, and the duration. + * + * m_currentPosition and m_paintCtx.translation and everything else not labeled "drawCoordinate" uses desktops as a unit. + * Exmp: 1.2 means the dekstop at index 1 shifted over by .2 desktops. + * All coords must be positive. + * + * For the wrapping effect, the render loop has to handle desktop coordinates larger than the total grid's width. + * 1. It uses modulus to keep the desktop coords in the range [0, gridWidth]. + * 2. It will draw the desktop at index 0 at index gridWidth if it has to. + * I will not draw any thing farther outside the range than that. + * + * I've put an explanation of all the important private vars down at the bottom. + * + * Good luck :) + */ + + class SlideEffect : public Effect { Q_OBJECT @@ -60,41 +85,50 @@ public: private Q_SLOTS: void desktopChanged(int old, int current, EffectWindow *with); + void desktopChanging(uint old, QPointF desktopOffset, EffectWindow* with); + void desktopChangingCancelled(); void windowAdded(EffectWindow *w); void windowDeleted(EffectWindow *w); private: - QPoint desktopCoords(int id) const; - QRect desktopGeometry(int id) const; - int workspaceWidth() const; - int workspaceHeight() const; - + QPoint getDrawCoords(QPointF pos, EffectScreen *screen); bool isTranslated(const EffectWindow *w) const; bool isPainted(const EffectWindow *w) const; bool shouldElevate(const EffectWindow *w) const; + QPointF moveInsideDesktopGrid(QPointF p); + QPointF constrainToDrawableRange(QPointF p); + QPointF forcePositivePosition(QPointF p) const; + void optimizePath(); //Find the best path to target desktop - void start(int old, int current, EffectWindow *movingWindow = nullptr); - void stop(); + void startAnimation(int old, int current, EffectWindow *movingWindow = nullptr); + void finishedSwitching(); private: int m_hGap; int m_vGap; bool m_slideDocks; bool m_slideBackground; + int m_fullAnimationDuration; // Miliseconds for 1 complete desktop switch bool m_active = false; TimeLine m_timeLine; - QPoint m_startPos; - QPoint m_diff; + + // When the desktop isn't desktopChanging(), these two variables are used to control the animation path. + // They use desktops as a unit. + QPointF m_startPos; + QPointF m_endPos; + EffectWindow *m_movingWindow = nullptr; std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero(); + bool m_gestureActive = false; // If we're currently animating a gesture + QPointF m_currentPosition; // Should always be kept up to date with where on the grid we're seeing. struct { int desktop; bool firstPass; bool lastPass; - QPoint translation; + QPointF translation; //Uses desktops as units QPoint currentPos; QVector visibleDesktops; @@ -106,7 +140,7 @@ private: inline int SlideEffect::duration() const { - return m_timeLine.duration().count(); + return m_fullAnimationDuration; } inline int SlideEffect::horizontalGap() const diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp index dc0faf9eac..58c17621c5 100644 --- a/src/virtualdesktops.cpp +++ b/src/virtualdesktops.cpp @@ -800,7 +800,7 @@ void VirtualDesktopManager::initShortcuts() // Gestures // These connections decide which desktop to end on after gesture ends connect(m_swipeGestureReleasedX.get(), &QAction::triggered, this, &VirtualDesktopManager::gestureReleasedX); - connect(m_swipeGestureReleasedY, &QAction::triggered, this, &VirtualDesktopManager::gestureReleasedY); + connect(m_swipeGestureReleasedY.get(), &QAction::triggered, this, &VirtualDesktopManager::gestureReleasedY); //These take the live feedback from a gesture input()->registerRealtimeTouchpadSwipeShortcut(SwipeDirection::Left, 3, m_swipeGestureReleasedX.get(), [this](qreal cb) {