Add new blend effect

When changing global settings like switching between light and dark
themes the result is currently quite messy. Apps update in a scattered
order and a jarring change.

The solution is heavily inspired from Gnome, we cache the window for X
milliseconds, then perform a crossfade between the cache and current
contents.

It does mean any video is paused for half a second, but pragmatically
it's not really bothersome.

On receipt of an explicit DBus method call we create a cache and start
animating. The explicit method call is needed so that we can guarantee
kwin has a fully copy before a client might change.

Use of an offscreen texture is used so that we include any window
decorations. The DeformEffect effect is repurposed as it has most the
relevnat logic if we remove the live updating.
This commit is contained in:
David Edmundson 2022-02-25 21:56:38 +00:00
parent a23d23593b
commit 9de9b93325
8 changed files with 238 additions and 4 deletions

View file

@ -86,6 +86,7 @@ add_subdirectory(touchpoints)
add_subdirectory(zoom)
# OpenGL-specific effects
add_subdirectory(blendchanges)
add_subdirectory(blur)
add_subdirectory(backgroundcontrast)
add_subdirectory(glide)

View file

@ -0,0 +1,14 @@
#######################################
# Effect
set(blendchanges_SOURCES
main.cpp
blendchanges.cpp
)
kwin4_add_effect_module(kwin4_effect_blend ${blendchanges_SOURCES})
target_link_libraries(kwin4_effect_blend PRIVATE
kwineffects
kwinglutils
Qt::DBus
)

View file

@ -0,0 +1,115 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2022 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
// own
#include "blendchanges.h"
#include "kwinglutils.h"
#include <QDBusConnection>
#include <QTimer>
namespace KWin
{
BlendChanges::BlendChanges()
: DeformEffect()
{
QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/BlendChanges"),
QStringLiteral("org.kde.KWin.BlendChanges"),
this,
QDBusConnection::ExportAllSlots);
setLive(false);
m_timeline.setEasingCurve(QEasingCurve::InOutCubic);
}
BlendChanges::~BlendChanges() = default;
bool BlendChanges::supported()
{
return effects->compositingType() == OpenGLCompositing && effects->animationsSupported();
}
void KWin::BlendChanges::start(int delay)
{
int animationDuration = animationTime(400);
if (!supported() || m_state != Off) {
return;
}
const EffectWindowList allWindows = effects->stackingOrder();
for (auto window : allWindows) {
redirect(window);
}
QTimer::singleShot(delay, this, [this, animationDuration]() {
m_timeline.setDuration(std::chrono::milliseconds(animationDuration));
effects->addRepaintFull();
m_state = Blending;
});
m_state = ShowingCache;
}
void BlendChanges::drawWindow(EffectWindow *window, int mask, const QRegion &region, WindowPaintData &data)
{
// draw the new picture underneath at full opacity
if (m_state != ShowingCache) {
Effect::drawWindow(window, mask, region, data);
}
// then the old on top, it works better than changing both alphas with the current blend mode
if (m_state != Off) {
DeformEffect::drawWindow(window, mask, region, data);
}
}
void BlendChanges::deform(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads)
{
Q_UNUSED(window)
Q_UNUSED(mask)
Q_UNUSED(quads)
data.setOpacity(1.0 - m_timeline.value());
}
bool BlendChanges::isActive() const
{
return m_state != Off;
}
void BlendChanges::postPaintScreen()
{
if (m_timeline.done()) {
m_lastPresentTime = std::chrono::milliseconds::zero();
m_timeline.reset();
m_state = Off;
const EffectWindowList allWindows = effects->stackingOrder();
for (auto window : allWindows) {
unredirect(window);
}
}
effects->addRepaintFull();
}
void BlendChanges::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
{
if (m_state == Off) {
return;
}
if (m_state == Blending) {
std::chrono::milliseconds delta(0);
if (m_lastPresentTime.count()) {
delta = presentTime - m_lastPresentTime;
}
m_lastPresentTime = presentTime;
m_timeline.update(delta);
}
effects->prePaintScreen(data, presentTime);
}
} // namespace KWin

View file

@ -0,0 +1,53 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2022 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <chrono>
#include <kwindeformeffect.h>
#include <kwineffects.h>
namespace KWin
{
class BlendChanges : public DeformEffect
{
Q_OBJECT
public:
BlendChanges();
~BlendChanges() override;
static bool supported();
// Effect interface
void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override;
void postPaintScreen() override;
void drawWindow(EffectWindow *window, int mask, const QRegion &region, WindowPaintData &data) override;
void deform(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) override;
bool isActive() const override;
public Q_SLOTS:
/**
* Called from DBus, this should be called before triggering any changes
* delay (ms) refers to how long to keep the current frame before starting a crossfade
* We should expect all clients to have repainted by the time this expires
*/
void start(int delay = 300);
private:
TimeLine m_timeline;
std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero();
enum State {
Off,
ShowingCache,
Blending
};
State m_state = Off;
};
} // namespace KWin

View file

@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2022 David Edmundson <davidedmundson@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "blendchanges.h"
namespace KWin
{
KWIN_EFFECT_FACTORY_SUPPORTED(BlendChanges,
"metadata.json.stripped",
return BlendChanges::supported();)
} // namespace KWin
#include "main.moc"

View file

@ -0,0 +1,13 @@
{
"KPlugin": {
"Name": "Blend Changes",
"Description": "Animates system style changes",
"Category": "Appearance",
"EnabledByDefault": true,
"Id": "blendchanges",
"License": "GPL"
},
"org.kde.kwin.effect": {
"internal": true
}
}

View file

@ -29,6 +29,7 @@ public:
const WindowPaintData &data, const WindowQuadList &quads);
GLTexture *maybeRender(EffectWindow *window, DeformOffscreenData *offscreenData);
bool live = true;
};
DeformEffect::DeformEffect(QObject *parent)
@ -47,6 +48,12 @@ bool DeformEffect::supported()
return effects->isOpenGLCompositing();
}
void DeformEffect::setLive(bool live)
{
Q_ASSERT(d->windows.isEmpty());
d->live = live;
}
void DeformEffect::redirect(EffectWindow *window)
{
DeformOffscreenData *&offscreenData = d->windows[window];
@ -58,6 +65,11 @@ void DeformEffect::redirect(EffectWindow *window)
if (d->windows.count() == 1) {
setupConnections();
}
if (!d->live) {
effects->makeOpenGLContextCurrent();
d->maybeRender(window, offscreenData);
}
}
void DeformEffect::unredirect(EffectWindow *window)
@ -205,8 +217,10 @@ void DeformEffect::handleWindowDeleted(EffectWindow *window)
void DeformEffect::setupConnections()
{
d->windowDamagedConnection =
connect(effects, &EffectsHandler::windowDamaged, this, &DeformEffect::handleWindowDamaged);
if (d->live) {
d->windowDamagedConnection =
connect(effects, &EffectsHandler::windowDamaged, this, &DeformEffect::handleWindowDamaged);
}
d->windowDeletedConnection =
connect(effects, &EffectsHandler::windowDeleted, this, &DeformEffect::handleWindowDeleted);
}

View file

@ -37,10 +37,16 @@ public:
static bool supported();
private:
void drawWindow(EffectWindow *window, int mask, const QRegion &region, WindowPaintData &data) override;
/**
* If set our offscreen texture will be updated with the latest contents
* It should be set before redirecting windows
* The default is true
*/
void setLive(bool live);
protected:
void drawWindow(EffectWindow *window, int mask, const QRegion &region, WindowPaintData &data) override;
/**
* This function must be called when the effect wants to animate the specified
* @a window.