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
This commit is contained in:
Eric Edlund 2022-03-22 18:48:43 -04:00 committed by Nate Graham
parent 26a4f75944
commit 003c820e00
3 changed files with 316 additions and 138 deletions

View file

@ -15,6 +15,8 @@
// KConfigSkeleton
#include "slideconfig.h"
#include <cmath>
namespace KWin
{
@ -27,21 +29,27 @@ SlideEffect::SlideEffect()
connect(effects, QOverload<int, int, EffectWindow *>::of(&EffectsHandler::desktopChanged),
this, &SlideEffect::desktopChanged);
connect(effects, QOverload<uint, QPointF, EffectWindow *>::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<SlideConfig>(500)));
m_fullAnimationDuration = animationTime<SlideConfig>(500);
m_timeLine.setDuration(std::chrono::milliseconds(m_fullAnimationDuration));
m_hGap = SlideConfig::horizontalGap();
m_vGap = SlideConfig::verticalGap();
@ -83,71 +91,68 @@ 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;
}
}
data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST;
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 &region, 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 &region, 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;
}
for (EffectScreen *screen: effects->screens()) {
QPoint translation = getDrawCoords(m_paintCtx.translation, screen);
if (isTranslated(w)) {
data += m_paintCtx.translation;
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;
if (p.x() < 0) {
p.setX(p.x() + std::ceil(-p.x() / effects->desktopGridWidth()) * effects->desktopGridWidth());
}
/**
* 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;
if (p.y() < 0) {
p.setY(p.y() + std::ceil(-p.y() / effects->desktopGridHeight()) * effects->desktopGridHeight());
}
/**
* 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;
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

View file

@ -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<int> visibleDesktops;
@ -106,7 +140,7 @@ private:
inline int SlideEffect::duration() const
{
return m_timeLine.duration().count();
return m_fullAnimationDuration;
}
inline int SlideEffect::horizontalGap() const

View file

@ -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) {