From ee88951b17c8400d4ca0471704bdedbdc6376d96 Mon Sep 17 00:00:00 2001 From: Vlad Zagorodniy Date: Thu, 28 Jun 2018 21:13:43 +0300 Subject: [PATCH] [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 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 --- autotests/libkwineffects/CMakeLists.txt | 1 + autotests/libkwineffects/timelinetest.cpp | 256 ++++++++++++++++++++++ libkwineffects/kwineffects.cpp | 148 +++++++++++++ libkwineffects/kwineffects.h | 199 ++++++++++++++++- 4 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 autotests/libkwineffects/timelinetest.cpp diff --git a/autotests/libkwineffects/CMakeLists.txt b/autotests/libkwineffects/CMakeLists.txt index 06ff1a50bc..ebbbcc2dc6 100644 --- a/autotests/libkwineffects/CMakeLists.txt +++ b/autotests/libkwineffects/CMakeLists.txt @@ -11,6 +11,7 @@ endmacro() kwineffects_unit_tests( windowquadlisttest + timelinetest ) add_executable(kwinglplatformtest kwinglplatformtest.cpp mock_gl.cpp ../../libkwineffects/kwinglplatform.cpp) diff --git a/autotests/libkwineffects/timelinetest.cpp b/autotests/libkwineffects/timelinetest.cpp new file mode 100644 index 0000000000..8483562962 --- /dev/null +++ b/autotests/libkwineffects/timelinetest.cpp @@ -0,0 +1,256 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2018 Vlad Zagorodniy + +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 . +*********************************************************************/ + +#include + +#include + +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("duration"); + QTest::addColumn("elapsed"); + QTest::addColumn("expectedElapsed"); + QTest::addColumn("expectedDone"); + QTest::addColumn("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" diff --git a/libkwineffects/kwineffects.cpp b/libkwineffects/kwineffects.cpp index 6c099e3881..86e13ffb6e 100644 --- a/libkwineffects/kwineffects.cpp +++ b/libkwineffects/kwineffects.cpp @@ -4,6 +4,7 @@ Copyright (C) 2006 Lubos Lunak Copyright (C) 2009 Lucas Murray +Copyright (C) 2018 Vlad Zagorodniy 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(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 diff --git a/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h index bf6beda729..6a66795904 100644 --- a/libkwineffects/kwineffects.h +++ b/libkwineffects/kwineffects.h @@ -5,6 +5,7 @@ Copyright (C) 2006 Lubos Lunak Copyright (C) 2009 Lucas Murray Copyright (C) 2010, 2011 Martin Gräßlin +Copyright (C) 2018 Vlad Zagorodniy 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 . #include #include +#include #include #include #include @@ -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 d; +}; + /** * Pointer to the global EffectsHandler object. **/ @@ -3512,6 +3708,7 @@ void Effect::initConfig() } // namespace Q_DECLARE_METATYPE(KWin::EffectWindow*) Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(KWin::TimeLine) /** @} */