[libkwineffects] Add TimeLine helper

Summary:
Most effects use QTimeLine in the following manner

```lang=cpp
if (...) {
    m_timeline->setCurrentTime(m_timeline->currentTime() + time);
} else {
    m_timeline->setCurrentTime(m_timeline->currentTime() - time);
}
```

Because effects do not rely on a timer that QTimeLine has, they can't
toggle direction of the QTimeLine, which makes somewhat harder to write
effects. In some cases that's obvious what condition to use to figure
out whether to add or subtract `time`, but there are cases when it's
not. In addition to that, setCurrentTime allows to have negative
currentTime, which in some cases causes bugs.

And overall, the way effects use QTimeLine is really hack-ish. It makes
more sense just to use an integer accumulator(like the Fall Apart
effect is doing) than to use QTimeLine.

Another problem with QTimeLine is that it's a QObject and some effects
do

```lang=cpp
class WindowInfo
{
public:
    ~WindowInfo();

    QTimeLine *timeLine;
};

WindowInfo::~WindowInfo()
{
    delete timeLine;
}

// ...

QHash<EffectWindow*, WindowInfo> m_windows;
```

which is unsafe.

This change adds the TimeLine class. The TimeLine class is a timeline
helper that designed specifically for needs of effects.

Demo

```lang=cpp
TimeLine timeLine(1000, TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);

timeLine.value(); // 0.0
timeLine.running(); // false
timeLine.done(); // false

timeLine.update(420);
timeLine.value(); // 0.42
timeLine.running(); // true
timeLine.done(); // false

timeLine.toggleDirection();
timeLine.value(); // 0.42
timeLine.running(); // true
timeLine.done(); // false

timeLine.update(100);
timeLine.value(); // 0.32
timeLine.running(); // true
timeLine.done(); // false

timeLine.update(1000);
timeLine.value(); // 0.0
timeLine.running(); // false
timeLine.done(); // true
```

Test Plan: Ran tests.

Reviewers: #kwin, davidedmundson, graesslin

Reviewed By: #kwin, davidedmundson, graesslin

Subscribers: romangg, graesslin, anthonyfieroni, davidedmundson, kwin

Tags: #kwin

Differential Revision: https://phabricator.kde.org/D13740
This commit is contained in:
Vlad Zagorodniy 2018-06-28 21:13:43 +03:00
parent 27cd0bc5d0
commit ee88951b17
4 changed files with 603 additions and 1 deletions

View file

@ -11,6 +11,7 @@ endmacro()
kwineffects_unit_tests(
windowquadlisttest
timelinetest
)
add_executable(kwinglplatformtest kwinglplatformtest.cpp mock_gl.cpp ../../libkwineffects/kwinglplatform.cpp)

View file

@ -0,0 +1,256 @@
/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include <kwineffects.h>
#include <QtTest>
using namespace std::chrono_literals;
// FIXME: Delete it in the future.
Q_DECLARE_METATYPE(std::chrono::milliseconds)
class TimeLineTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testUpdateForward();
void testUpdateBackward();
void testUpdateFinished();
void testToggleDirection();
void testReset();
void testSetElapsed_data();
void testSetElapsed();
void testSetDuration();
void testSetDurationRetargeting();
void testSetDurationRetargetingSmallDuration();
void testRunning();
};
void TimeLineTest::testUpdateForward()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
// 0/1000
QCOMPARE(timeLine.value(), 0.0);
QVERIFY(!timeLine.done());
// 100/1000
timeLine.update(100ms);
QCOMPARE(timeLine.value(), 0.1);
QVERIFY(!timeLine.done());
// 400/1000
timeLine.update(300ms);
QCOMPARE(timeLine.value(), 0.4);
QVERIFY(!timeLine.done());
// 900/1000
timeLine.update(500ms);
QCOMPARE(timeLine.value(), 0.9);
QVERIFY(!timeLine.done());
// 1000/1000
timeLine.update(3000ms);
QCOMPARE(timeLine.value(), 1.0);
QVERIFY(timeLine.done());
}
void TimeLineTest::testUpdateBackward()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Backward);
timeLine.setEasingCurve(QEasingCurve::Linear);
// 0/1000
QCOMPARE(timeLine.value(), 1.0);
QVERIFY(!timeLine.done());
// 100/1000
timeLine.update(100ms);
QCOMPARE(timeLine.value(), 0.9);
QVERIFY(!timeLine.done());
// 400/1000
timeLine.update(300ms);
QCOMPARE(timeLine.value(), 0.6);
QVERIFY(!timeLine.done());
// 900/1000
timeLine.update(500ms);
QCOMPARE(timeLine.value(), 0.1);
QVERIFY(!timeLine.done());
// 1000/1000
timeLine.update(3000ms);
QCOMPARE(timeLine.value(), 0.0);
QVERIFY(timeLine.done());
}
void TimeLineTest::testUpdateFinished()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
timeLine.update(1000ms);
QCOMPARE(timeLine.value(), 1.0);
QVERIFY(timeLine.done());
timeLine.update(42ms);
QCOMPARE(timeLine.value(), 1.0);
QVERIFY(timeLine.done());
}
void TimeLineTest::testToggleDirection()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
QCOMPARE(timeLine.value(), 0.0);
QVERIFY(!timeLine.done());
timeLine.update(600ms);
QCOMPARE(timeLine.value(), 0.6);
QVERIFY(!timeLine.done());
timeLine.toggleDirection();
QCOMPARE(timeLine.value(), 0.6);
QVERIFY(!timeLine.done());
timeLine.update(200ms);
QCOMPARE(timeLine.value(), 0.4);
QVERIFY(!timeLine.done());
timeLine.update(3000ms);
QCOMPARE(timeLine.value(), 0.0);
QVERIFY(timeLine.done());
}
void TimeLineTest::testReset()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
timeLine.update(1000ms);
QCOMPARE(timeLine.value(), 1.0);
QVERIFY(timeLine.done());
timeLine.reset();
QCOMPARE(timeLine.value(), 0.0);
QVERIFY(!timeLine.done());
}
void TimeLineTest::testSetElapsed_data()
{
QTest::addColumn<std::chrono::milliseconds>("duration");
QTest::addColumn<std::chrono::milliseconds>("elapsed");
QTest::addColumn<std::chrono::milliseconds>("expectedElapsed");
QTest::addColumn<bool>("expectedDone");
QTest::addColumn<bool>("initiallyDone");
QTest::newRow("Less than duration, not finished") << 1000ms << 300ms << 300ms << false << false;
QTest::newRow("Less than duration, finished") << 1000ms << 300ms << 300ms << false << true;
QTest::newRow("Greater than duration, not finished") << 1000ms << 3000ms << 1000ms << true << false;
QTest::newRow("Greater than duration, finished") << 1000ms << 3000ms << 1000ms << true << true;
QTest::newRow("Equal to duration, not finished") << 1000ms << 1000ms << 1000ms << true << false;
QTest::newRow("Equal to duration, finished") << 1000ms << 1000ms << 1000ms << true << true;
}
void TimeLineTest::testSetElapsed()
{
QFETCH(std::chrono::milliseconds, duration);
QFETCH(std::chrono::milliseconds, elapsed);
QFETCH(std::chrono::milliseconds, expectedElapsed);
QFETCH(bool, expectedDone);
QFETCH(bool, initiallyDone);
KWin::TimeLine timeLine(duration, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
if (initiallyDone) {
timeLine.update(duration);
QVERIFY(timeLine.done());
}
timeLine.setElapsed(elapsed);
QCOMPARE(timeLine.elapsed(), expectedElapsed);
QCOMPARE(timeLine.done(), expectedDone);
}
void TimeLineTest::testSetDuration()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
QCOMPARE(timeLine.duration(), 1000ms);
timeLine.setDuration(3000ms);
QCOMPARE(timeLine.duration(), 3000ms);
}
void TimeLineTest::testSetDurationRetargeting()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
timeLine.update(500ms);
QCOMPARE(timeLine.value(), 0.5);
QVERIFY(!timeLine.done());
timeLine.setDuration(3000ms);
QCOMPARE(timeLine.value(), 0.5);
QVERIFY(!timeLine.done());
}
void TimeLineTest::testSetDurationRetargetingSmallDuration()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
timeLine.update(999ms);
QCOMPARE(timeLine.value(), 0.999);
QVERIFY(!timeLine.done());
timeLine.setDuration(3ms);
QCOMPARE(timeLine.value(), 1.0);
QVERIFY(timeLine.done());
}
void TimeLineTest::testRunning()
{
KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward);
timeLine.setEasingCurve(QEasingCurve::Linear);
QVERIFY(!timeLine.running());
QVERIFY(!timeLine.done());
timeLine.update(100ms);
QVERIFY(timeLine.running());
QVERIFY(!timeLine.done());
timeLine.update(900ms);
QVERIFY(!timeLine.running());
QVERIFY(timeLine.done());
}
QTEST_MAIN(TimeLineTest)
#include "timelinetest.moc"

View file

@ -4,6 +4,7 @@
Copyright (C) 2006 Lubos Lunak <l.lunak@kde.org>
Copyright (C) 2009 Lucas Murray <lmurray@undefinedfire.com>
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -1919,5 +1920,152 @@ void EffectFrame::setScreenProjectionMatrix(const QMatrix4x4 &spm)
d->screenProjectionMatrix = spm;
}
/***************************************************************
TimeLine
***************************************************************/
class Q_DECL_HIDDEN TimeLine::Data : public QSharedData
{
public:
std::chrono::milliseconds duration;
Direction direction;
QEasingCurve easingCurve;
std::chrono::milliseconds elapsed = std::chrono::milliseconds::zero();
bool done = false;
};
TimeLine::TimeLine(std::chrono::milliseconds duration, Direction direction)
: d(new Data)
{
Q_ASSERT(duration > std::chrono::milliseconds::zero());
d->duration = duration;
d->direction = direction;
}
TimeLine::TimeLine(const TimeLine &other)
: d(other.d)
{
}
TimeLine::~TimeLine() = default;
qreal TimeLine::progress() const
{
return static_cast<qreal>(d->elapsed.count()) / d->duration.count();
}
qreal TimeLine::value() const
{
const qreal t = progress();
return d->easingCurve.valueForProgress(
d->direction == Backward ? 1.0 - t : t);
}
void TimeLine::update(std::chrono::milliseconds delta)
{
Q_ASSERT(delta >= std::chrono::milliseconds::zero());
if (d->done) {
return;
}
d->elapsed += delta;
if (d->elapsed >= d->duration) {
d->done = true;
d->elapsed = d->duration;
}
}
std::chrono::milliseconds TimeLine::elapsed() const
{
return d->elapsed;
}
void TimeLine::setElapsed(std::chrono::milliseconds elapsed)
{
Q_ASSERT(elapsed >= std::chrono::milliseconds::zero());
if (elapsed == d->elapsed) {
return;
}
reset();
update(elapsed);
}
std::chrono::milliseconds TimeLine::duration() const
{
return d->duration;
}
void TimeLine::setDuration(std::chrono::milliseconds duration)
{
Q_ASSERT(duration > std::chrono::milliseconds::zero());
if (duration == d->duration) {
return;
}
d->elapsed = std::chrono::milliseconds(qRound(progress() * duration.count()));
d->duration = duration;
if (d->elapsed == d->duration) {
d->done = true;
}
}
TimeLine::Direction TimeLine::direction() const
{
return d->direction;
}
void TimeLine::setDirection(TimeLine::Direction direction)
{
if (d->direction == direction) {
return;
}
if (d->elapsed > std::chrono::milliseconds::zero()) {
d->elapsed = d->duration - d->elapsed;
}
d->direction = direction;
}
void TimeLine::toggleDirection()
{
setDirection(d->direction == Forward ? Backward : Forward);
}
QEasingCurve TimeLine::easingCurve() const
{
return d->easingCurve;
}
void TimeLine::setEasingCurve(const QEasingCurve &easingCurve)
{
d->easingCurve = easingCurve;
}
void TimeLine::setEasingCurve(QEasingCurve::Type type)
{
d->easingCurve.setType(type);
}
bool TimeLine::running() const
{
return d->elapsed != std::chrono::milliseconds::zero()
&& d->elapsed != d->duration;
}
bool TimeLine::done() const
{
return d->done;
}
void TimeLine::reset()
{
d->elapsed = std::chrono::milliseconds::zero();
d->done = false;
}
TimeLine &TimeLine::operator=(const TimeLine &other)
{
d = other.d;
return *this;
}
} // namespace

View file

@ -5,6 +5,7 @@
Copyright (C) 2006 Lubos Lunak <l.lunak@kde.org>
Copyright (C) 2009 Lucas Murray <lmurray@undefinedfire.com>
Copyright (C) 2010, 2011 Martin Gräßlin <mgraesslin@kde.org>
Copyright (C) 2018 Vlad Zagorodniy <vladzzag@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -27,6 +28,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <kwineffects_export.h>
#include <kwinglobals.h>
#include <QEasingCurve>
#include <QIcon>
#include <QPair>
#include <QSet>
@ -186,7 +188,7 @@ X-KDE-Library=kwin4_effect_cooleffect
#define KWIN_EFFECT_API_MAKE_VERSION( major, minor ) (( major ) << 8 | ( minor ))
#define KWIN_EFFECT_API_VERSION_MAJOR 0
#define KWIN_EFFECT_API_VERSION_MINOR 225
#define KWIN_EFFECT_API_VERSION_MINOR 226
#define KWIN_EFFECT_API_VERSION KWIN_EFFECT_API_MAKE_VERSION( \
KWIN_EFFECT_API_VERSION_MAJOR, KWIN_EFFECT_API_VERSION_MINOR )
@ -3283,6 +3285,200 @@ private:
EffectFramePrivate* const d;
};
/**
* The TimeLine class is a helper for controlling animations.
**/
class KWINEFFECTS_EXPORT TimeLine
{
public:
/**
* Direction of the timeline.
*
* When the direction of the timeline is Forward, the progress
* value will go from 0.0 to 1.0.
*
* When the direction of the timeline is Backward, the progress
* value will go from 1.0 to 0.0.
**/
enum Direction {
Forward,
Backward
};
/**
* Constructs a new instance of TimeLine.
*
* @param duration Duration of the timeline, in milliseconds
* @param direction Direction of the timeline
* @since 5.14
**/
explicit TimeLine(std::chrono::milliseconds duration = std::chrono::milliseconds(1000),
Direction direction = Forward);
TimeLine(const TimeLine &other);
~TimeLine();
/**
* Returns the current value of the timeline.
*
* @since 5.14
**/
qreal value() const;
/**
* Updates the progress of the timeline.
*
* @note The delta value should be a non-negative number, i.e. it
* should be greater or equal to 0.
*
* @param delta The number milliseconds passed since last frame
* @since 5.14
**/
void update(std::chrono::milliseconds delta);
/**
* Returns the number of elapsed milliseconds.
*
* @see setElapsed
* @since 5.14
**/
std::chrono::milliseconds elapsed() const;
/**
* Sets the number of elapsed milliseconds.
*
* This method overwrites previous value of elapsed milliseconds.
* If the new value of elapsed milliseconds is greater or equal
* to duration of the timeline, the timeline will be finished, i.e.
* proceeding TimeLine::done method calls will return @c true.
* Please don't use it. Instead, use TimeLine::update.
*
* @note The new number of elapsed milliseconds should be a non-negative
* number, i.e. it should be greater or equal to 0.
*
* @param elapsed The new number of elapsed milliseconds
* @see elapsed
* @since 5.14
**/
void setElapsed(std::chrono::milliseconds elapsed);
/**
* Returns the duration of the timeline.
*
* @returns Duration of the timeline, in milliseconds
* @see setDuration
* @since 5.14
**/
std::chrono::milliseconds duration() const;
/**
* Sets the duration of the timeline.
*
* In addition to setting new value of duration, the timeline will
* try to retarget the number of elapsed milliseconds to match
* as close as possible old progress value. If the new duration
* is much smaller than old duration, there is a big chance that
* the timeline will be finished after setting new duration.
*
* @note The new duration should be a positive number, i.e. it
* should be greater or equal to 1.
*
* @param duration The new duration of the timeline, in milliseconds
* @see duration
* @since 5.14
**/
void setDuration(std::chrono::milliseconds duration);
/**
* Returns the direction of the timeline.
*
* @returns Direction of the timeline(TimeLine::Forward or TimeLine::Backward)
* @see setDirection
* @see toggleDirection
* @since 5.14
**/
Direction direction() const;
/**
* Sets the direction of the timeline.
*
* @param direction The new direction of the timeline
* @see direction
* @see toggleDirection
* @since 5.14
**/
void setDirection(Direction direction);
/**
* Toggles the direction of the timeline.
*
* If the direction of the timeline was TimeLine::Forward, it becomes
* TimeLine::Backward, and vice verca.
*
* @see direction
* @see setDirection
* @since 5.14
**/
void toggleDirection();
/**
* Returns the easing curve of the timeline.
*
* @see setEasingCurve
* @since 5.14
**/
QEasingCurve easingCurve() const;
/**
* Sets new easing curve.
*
* @param easingCurve An easing curve to be set
* @see easingCurve
* @since 5.14
**/
void setEasingCurve(const QEasingCurve &easingCurve);
/**
* Sets new easing curve by providing its type.
*
* @param type Type of the easing curve(e.g. QEasingCurve::InQuad, etc)
* @see easingCurve
* @since 5.14
**/
void setEasingCurve(QEasingCurve::Type type);
/**
* Returns whether the timeline is currently in progress.
*
* @see done
* @since 5.14
**/
bool running() const;
/**
* Returns whether the timeline is finished.
*
* @see reset
* @since 5.14
**/
bool done() const;
/**
* Resets the timeline to initial state.
*
* @since 5.14
**/
void reset();
TimeLine &operator=(const TimeLine &other);
private:
qreal progress() const;
private:
class Data;
QSharedDataPointer<Data> d;
};
/**
* Pointer to the global EffectsHandler object.
**/
@ -3512,6 +3708,7 @@ void Effect::initConfig()
} // namespace
Q_DECLARE_METATYPE(KWin::EffectWindow*)
Q_DECLARE_METATYPE(QList<KWin::EffectWindow*>)
Q_DECLARE_METATYPE(KWin::TimeLine)
/** @} */