diff --git a/cmake/FindFFTW.cmake b/cmake/FindFFTW.cmake new file mode 100644 --- /dev/null +++ b/cmake/FindFFTW.cmake @@ -0,0 +1,20 @@ +# Find the FFTW library +# +# Usage: +# find_package(FFTW [REQUIRED]) +# +# It sets the following variables: +# FFTW_FOUND +# FFTW_INCLUDES +# FFTW_LIBRARIES + + +find_path(FFTW_INCLUDES fftw3.h) + +find_library(FFTW_LIBRARIES NAMES fftw3) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(FFTW DEFAULT_MSG + FFTW_INCLUDES FFTW_LIBRARIES) + +mark_as_advanced(FFTW_INCLUDES FFTW_LIBRARIES) diff --git a/kdecoration/CMakeLists.txt b/kdecoration/CMakeLists.txt --- a/kdecoration/CMakeLists.txt +++ b/kdecoration/CMakeLists.txt @@ -12,6 +12,9 @@ PURPOSE "Required to pass style properties to native Windows on X11 Platform" ) +### FFTW3 +find_package(FFTW REQUIRED) + if(UNIX AND NOT APPLE) set(BREEZE_HAVE_X11 ${XCB_XCB_FOUND}) @@ -33,6 +36,7 @@ set(breezedecoration_SRCS breezebutton.cpp breezedecoration.cpp + breezeboxshadowhelper.cpp breezeexceptionlist.cpp breezesettingsprovider.cpp breezesizegrip.cpp) @@ -86,6 +90,7 @@ XCB::XCB) endif() +target_link_libraries(breezedecoration PRIVATE ${FFTW_LIBRARIES}) install(TARGETS breezedecoration DESTINATION ${PLUGIN_INSTALL_DIR}/org.kde.kdecoration2) install(FILES config/breezedecorationconfig.desktop DESTINATION ${SERVICES_INSTALL_DIR}) diff --git a/kdecoration/breezeboxshadowhelper.h b/kdecoration/breezeboxshadowhelper.h new file mode 100644 --- /dev/null +++ b/kdecoration/breezeboxshadowhelper.h @@ -0,0 +1,37 @@ +/* + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#ifndef BREEZE_BOXSHADOWHELPER_H +#define BREEZE_BOXSHADOWHELPER_H + +#include +#include +#include + + +namespace Breeze { +namespace BoxShadowHelper { + +void boxShadow(QPainter *p, const QRect &box, int radius, const QColor &color); + +} // BoxShadowHelper +} // Breeze + +#endif // BREEZE_BOXSHADOWHELPER_H diff --git a/kdecoration/breezeboxshadowhelper.cpp b/kdecoration/breezeboxshadowhelper.cpp new file mode 100644 --- /dev/null +++ b/kdecoration/breezeboxshadowhelper.cpp @@ -0,0 +1,251 @@ +/* + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 "breezeboxshadowhelper.h" + +#include + +#include +#include + +#include + + +namespace Breeze { +namespace BoxShadowHelper { + +QVector computeGaussianKernel(double radius, double sigma) +{ + QVector kernel; + const int kernelSize = static_cast(radius) * 2 + 1; + + const double den = sqrt(2.0) * sigma; + double kernelNorm = 0.0; + double lastInt = 0.5 * erf((-radius - 0.5) / den); + + for (int i = 0; i < kernelSize; i++) { + const double currInt = 0.5 * erf((i - radius + 0.5) / den); + const double w = currInt - lastInt; + kernel << w; + kernelNorm += w; + lastInt = currInt; + } + + for (auto &w : kernel) { + w /= kernelNorm; + } + + return kernel; +} + +// Blur alpha channel of the given image using separable convolution +// gaussian kernel. Not very efficient with big blur radii. +void blurAlphaSeparable(QImage &img, double radius, double sigma) +{ + const auto kernel = computeGaussianKernel(radius, sigma); + + QImage tmp(img.size().transposed(), img.format()); + + QRgb *imgData = reinterpret_cast(img.scanLine(0)); + QRgb *tmpData = reinterpret_cast(tmp.scanLine(0)); + const int imgStride = img.width(); + const int tmpStride = tmp.width(); + + const int shift = static_cast(radius); + + // Blur in X direction. Please note, the result is stored + // in a temporary transposed temporary buffer. The result + // is transposed in order to read memory in linear order. + for (int y = 0; y < img.height(); y++) { + for (int x = 0; x < img.width(); x++) { + double alpha = 0.0; + + for (int i = 0; i < kernel.size(); i++) { + const int idx = y * imgStride + qBound(0, x + i - shift, img.width() - 1); + alpha += qAlpha(imgData[idx]) * kernel[i]; + } + + const int idx = x * tmpStride + y; + tmpData[idx] = qRgba(0, 0, 0, static_cast(alpha)); + } + } + + // Blur in Y direction. The result is transposed again so size + // matches original image size. + for (int y = 0; y < tmp.height(); y++) { + for (int x = 0; x < tmp.width(); x++) { + double alpha = 0.0; + + for (int i = 0; i < kernel.size(); i++) { + const int idx = y * tmpStride + qBound(0, x + i - shift, tmp.width() - 1); + alpha += qAlpha(tmpData[idx]) * kernel[i]; + } + + const int idx = x * imgStride + y; + imgData[idx] = qRgba(0, 0, 0, static_cast(alpha)); + } + } +} + +// Blur alpha channel of the given image using Fourier Transform. +// It's somewhat efficient with big blur radii. +// +// It works as follows: +// - do FFT on given input image(it is expected, that the +// input image was padded before) +// - compute Gaussian kernel, pad it to the size of the input +// image, and do FFT on it +// - multiply the two in the frequency domain(element-wise) +// - transform the result back to "time domain" +// +// Please notice that in order to omit several(4, more precisely) +// memory copy ops, the Gaussian kernel is wrapped around and not centered. +void blurAlphaFFT(QImage &img, double radius, double sigma) +{ + int size = img.width() * img.height(); + + fftw_complex *imageIn; + imageIn = reinterpret_cast(fftw_malloc(sizeof(fftw_complex) * size)); + + QRgb *imgData = reinterpret_cast(img.scanLine(0)); + for (int i = 0; i < size; i++) { + imageIn[i][0] = qAlpha(imgData[i]); + imageIn[i][1] = 0.0; + } + + fftw_complex *imageOut; + imageOut = reinterpret_cast(fftw_malloc(sizeof(fftw_complex) * size)); + + QVector kernel_ = computeGaussianKernel(radius, sigma); + QVector kernel; + kernel.resize(size); + + const int shift = -static_cast(radius); + const int kernelSize = kernel_.size(); + for (int y = 0; y < kernelSize; y++) { + for (int x = 0; x < kernelSize; x++) { + int j = (img.width() + x + shift) % img.width(); + int i = (img.height() + y + shift) % img.height(); + kernel[j + i * img.width()] = kernel_[x] * kernel_[y]; + } + } + + fftw_complex *kernelIn; + kernelIn = reinterpret_cast(fftw_malloc(sizeof(fftw_complex) * kernel.size())); + + for (int i = 0; i < size; i++) { + kernelIn[i][0] = kernel[i]; + kernelIn[i][1] = 0.0; + } + + fftw_complex *kernelOut; + kernelOut = reinterpret_cast(fftw_malloc(sizeof(fftw_complex) * kernel.size())); + + fftw_plan planImageFFT; + planImageFFT = fftw_plan_dft_2d(img.height(), img.width(), + imageIn, imageOut, + FFTW_FORWARD, FFTW_ESTIMATE); + fftw_execute(planImageFFT); + + fftw_plan planKernelFFT; + planKernelFFT = fftw_plan_dft_2d(img.height(), img.width(), + kernelIn, kernelOut, + FFTW_FORWARD, FFTW_ESTIMATE); + fftw_execute(planKernelFFT); + + for (int i = 0; i < size; i++) { + double re = imageOut[i][0] * kernelOut[i][0] - imageOut[i][1] * kernelOut[i][1]; + double im = imageOut[i][0] * kernelOut[i][1] + imageOut[i][1] * kernelOut[i][0]; + imageOut[i][0] = re; + imageOut[i][1] = im; + } + + fftw_plan planImageIFFT; + planImageIFFT = fftw_plan_dft_2d(img.height(), img.width(), + imageOut, imageIn, + FFTW_BACKWARD, FFTW_ESTIMATE); + fftw_execute(planImageIFFT); + + for (int i = 0; i < size; i++) { + imgData[i] = qRgba(0, 0, 0, imageIn[i][0] / size); + } + + fftw_free(kernelIn); + fftw_free(kernelOut); + + fftw_free(imageIn); + fftw_free(imageOut); + + fftw_destroy_plan(planKernelFFT); + fftw_destroy_plan(planImageFFT); + fftw_destroy_plan(planImageIFFT); +} + +namespace { + // FFT approach outperforms separable convolution kernels when blur radius >= 64. + // (was discovered after doing a lot of benchmarks) + const int FFT_BLUR_RADIUS_THRESHOLD = 64; + + // With big sigma scale, it is not enough to pad box image with blur radius. + // As a workaround, we could lower the scale value. According to the CSS + // Level 3 spec, standard deviation must be equal to half of the blur radius. + // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur + const double SIGMA_BLUR_SCALE = 0.4375; +} + +inline double radiusToSigma(double radius) +{ + return SIGMA_BLUR_SCALE * radius + 0.5; +} + +void boxShadow(QPainter *p, const QRect &box, int radius, const QColor &color) +{ + QSize size = box.size() + 2 * QSize(radius, radius); + + QPainter painter; + + QImage shadow(size, QImage::Format_ARGB32_Premultiplied); + shadow.fill(Qt::transparent); + painter.begin(&shadow); + painter.fillRect(QRect(QPoint(radius, radius), box.size()), Qt::black); + painter.end(); + + double sigma = radiusToSigma(radius); + + // There is no need to blur RGB channels. Blur the alpha + // channel and do compositing stuff later. + if (radius < FFT_BLUR_RADIUS_THRESHOLD) { + blurAlphaSeparable(shadow, radius, sigma); + } else { + blurAlphaFFT(shadow, radius, sigma); + } + + painter.begin(&shadow); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(shadow.rect(), color); + painter.end(); + + QRect shadowRect = shadow.rect(); + shadowRect.moveCenter(box.center()); + p->drawImage(shadowRect, shadow); +} + +} // BoxShadowHelper +} // Breeze diff --git a/kdecoration/breezedecoration.h b/kdecoration/breezedecoration.h --- a/kdecoration/breezedecoration.h +++ b/kdecoration/breezedecoration.h @@ -119,7 +119,7 @@ void createButtons(); void paintTitleBar(QPainter *painter, const QRect &repaintRegion); - void createShadow(); + void updateShadow(bool forced = false); //*@name border size //@{ diff --git a/kdecoration/breezedecoration.cpp b/kdecoration/breezedecoration.cpp --- a/kdecoration/breezedecoration.cpp +++ b/kdecoration/breezedecoration.cpp @@ -29,6 +29,8 @@ #include "breezebutton.h" #include "breezesizegrip.h" +#include "breezeboxshadowhelper.h" + #include #include #include @@ -66,10 +68,86 @@ //________________________________________________________________ static int g_sDecoCount = 0; static int g_shadowSizeEnum = InternalSettings::ShadowLarge; - static int g_shadowStrength = 90; + static int g_shadowStrength = 204; static QColor g_shadowColor = Qt::black; static QSharedPointer g_sShadow; + struct ShadowParam { + struct { + int left; + int top; + } offset; + + struct { + int radius; + double opacity; + } shadow1 + , shadow2; + }; + + static const ShadowParam s_shadowParams[] = { + { + // Small + .offset = { + .left = 0, + .top = 6 + }, + .shadow1 = { + .radius = 16, + .opacity = 1.0 + }, + .shadow2 = { + .radius = 4, + .opacity = 0.1 + } + }, + { + // Medium + .offset = { + .left = 0, + .top = 12 + }, + .shadow1 = { + .radius = 32, + .opacity = 1.0 + }, + .shadow2 = { + .radius = 16, + .opacity = 0.1 + } + }, + { + // Large + .offset = { + .left = 0, + .top = 20 + }, + .shadow1 = { + .radius = 64, + .opacity = 1.0 + }, + .shadow2 = { + .radius = 24, + .opacity = 0.1 + } + }, + { + // Very large + .offset = { + .left = 0, + .top = 34 + }, + .shadow1 = { + .radius = 96, + .opacity = 1.0 + }, + .shadow2 = { + .radius = 28, + .opacity = 0.1 + } + } + }; + //________________________________________________________________ Decoration::Decoration(QObject *parent, const QVariantList &args) : KDecoration2::Decoration(parent, args) @@ -201,7 +279,7 @@ connect(c, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateButtonsGeometry); createButtons(); - createShadow(); + updateShadow(true); } //________________________________________________________________ @@ -293,7 +371,7 @@ recalculateBorders(); // shadow - createShadow(); + updateShadow(); // size grip if( hasNoBorders() && m_internalSettings->drawSizeGrip() ) createSizeGrip(); @@ -620,107 +698,108 @@ } //________________________________________________________________ - void Decoration::createShadow() + void Decoration::updateShadow(bool forced) { - - // assign global shadow if exists and parameters match - if( - !g_sShadow || - g_shadowSizeEnum != m_internalSettings->shadowSize() || - g_shadowStrength != m_internalSettings->shadowStrength() || - g_shadowColor != m_internalSettings->shadowColor() - ) + if (g_shadowSizeEnum != m_internalSettings->shadowSize() + || g_shadowStrength != m_internalSettings->shadowStrength() + || g_shadowColor != m_internalSettings->shadowColor() + || forced) { - // assign parameters g_shadowSizeEnum = m_internalSettings->shadowSize(); g_shadowStrength = m_internalSettings->shadowStrength(); g_shadowColor = m_internalSettings->shadowColor(); - // shadow size from enum - int shadowSize = 0; - switch( g_shadowSizeEnum ) - { - default: - case InternalSettings::ShadowLarge: shadowSize = 64; break; - - case InternalSettings::ShadowNone: shadowSize = Metrics::Shadow_Overlap + 1; break; - case InternalSettings::ShadowSmall: shadowSize = 16; break; - case InternalSettings::ShadowMedium: shadowSize = 32; break; - case InternalSettings::ShadowVeryLarge: shadowSize = 96; break; + ShadowParam params; + + switch (g_shadowSizeEnum) { + case InternalSettings::ShadowNone: + g_sShadow.clear(); + setShadow(g_sShadow); + return; + case InternalSettings::ShadowSmall: + params = s_shadowParams[0]; + break; + case InternalSettings::ShadowMedium: + params = s_shadowParams[1]; + break; + case InternalSettings::ShadowLarge: + params = s_shadowParams[2]; + break; + case InternalSettings::ShadowVeryLarge: + params = s_shadowParams[3]; + break; + default: + // fallback to Large + params = s_shadowParams[2]; + break; } - // offset - int shadowOffset = (g_shadowSizeEnum == InternalSettings::ShadowNone) ? 0 : qMax( 6*shadowSize/16, Metrics::Shadow_Overlap*2 ); - - // create image - QImage image(2*shadowSize, 2*shadowSize, QImage::Format_ARGB32_Premultiplied); - image.fill(Qt::transparent); - - // painter - QPainter painter(&image); - painter.setRenderHint( QPainter::Antialiasing, true ); - - // color calculation delta function - auto gradientStopColor = [](QColor color, int alpha) - { - color.setAlpha(alpha); - return color; + auto withAlpha = [](const QColor &color, int alpha) -> QColor { + QColor c(color); + c.setAlpha(alpha); + return c; }; - // create gradient - if( g_shadowSizeEnum != InternalSettings::ShadowNone ) - { - - // gaussian lambda function - auto alpha = [](qreal x) { return std::exp( -x*x/0.15 ); }; - - QRadialGradient radialGradient( shadowSize, shadowSize, shadowSize ); - for( int i = 0; i < 10; ++i ) - { - const qreal x( qreal( i )/9 ); - radialGradient.setColorAt(x, gradientStopColor( g_shadowColor, alpha(x)*g_shadowStrength ) ); - } - - radialGradient.setColorAt(1, gradientStopColor( g_shadowColor, 0 ) ); - - // fill - painter.fillRect( image.rect(), radialGradient); - - } - - // contrast pixel - QRectF innerRect = QRectF( - shadowSize - Metrics::Shadow_Overlap, shadowSize - shadowOffset - Metrics::Shadow_Overlap, - 2*Metrics::Shadow_Overlap, shadowOffset + 2*Metrics::Shadow_Overlap ); - - painter.setPen( gradientStopColor( g_shadowColor, (g_shadowSizeEnum == InternalSettings::ShadowNone) ? g_shadowStrength:(g_shadowStrength*0.5) ) ); - painter.setBrush( Qt::NoBrush ); - painter.drawRoundedRect( innerRect, -0.5 + Metrics::Frame_FrameRadius, -0.5 + Metrics::Frame_FrameRadius ); - - // mask out inner rect - painter.setPen( Qt::NoPen ); - painter.setBrush( Qt::black ); - painter.setCompositionMode(QPainter::CompositionMode_DestinationOut ); - painter.drawRoundedRect( innerRect, 0.5 + Metrics::Frame_FrameRadius, 0.5 + Metrics::Frame_FrameRadius ); + // In order to properly render a box shadow with a given radius `shadowSize`, + // the box size should be at least `2 * QSize(shadowSize, shadowSize)` + int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); + QRect box(shadowSize, shadowSize, 2 * shadowSize + 1, 2 * shadowSize + 1); + QRect rect = box.adjusted(-shadowSize, -shadowSize, shadowSize, shadowSize); + + QImage shadow(rect.size(), QImage::Format_ARGB32_Premultiplied); + shadow.fill(Qt::transparent); + QPainter painter(&shadow); + painter.setRenderHint(QPainter::Antialiasing); + + double strength = static_cast(g_shadowStrength) / 255.0; + + // Draw "shape" shadow. + BoxShadowHelper::boxShadow( + &painter, + box, + params.shadow1.radius, + withAlpha(g_shadowColor, static_cast(255 * params.shadow1.opacity * strength))); + + // Draw "contrast" shadow. + BoxShadowHelper::boxShadow( + &painter, + box, + params.shadow2.radius, + withAlpha(g_shadowColor, static_cast(255 * params.shadow2.opacity * strength))); + + // Mask out inner rect. + QMargins padding = QMargins( + shadowSize - Metrics::Shadow_Overlap - params.offset.left, + shadowSize - Metrics::Shadow_Overlap - params.offset.top, + shadowSize - Metrics::Shadow_Overlap + params.offset.left, + shadowSize - Metrics::Shadow_Overlap + params.offset.top); + + painter.setPen(Qt::NoPen); + painter.setBrush(Qt::black); + painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); + painter.drawRoundedRect( + rect - padding, + Metrics::Frame_FrameRadius + 0.5, + Metrics::Frame_FrameRadius + 0.5); + + // Draw pixel contrast. + painter.setPen(withAlpha(g_shadowColor, 50 * strength)); + painter.setBrush(Qt::NoBrush); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.drawRoundedRect( + rect - padding, + Metrics::Frame_FrameRadius - 0.5, + Metrics::Frame_FrameRadius - 0.5); painter.end(); g_sShadow = QSharedPointer::create(); - g_sShadow->setPadding( QMargins( - shadowSize - Metrics::Shadow_Overlap, - shadowSize - shadowOffset - Metrics::Shadow_Overlap, - shadowSize - Metrics::Shadow_Overlap, - shadowSize - Metrics::Shadow_Overlap ) ); - - g_sShadow->setInnerShadowRect(QRect( shadowSize, shadowSize, 1, 1) ); - - // assign image - g_sShadow->setShadow(image); - + g_sShadow->setPadding(padding); + g_sShadow->setInnerShadowRect(QRect(rect.center(), QSize(1, 1))); + g_sShadow->setShadow(shadow); } setShadow(g_sShadow); - } //_________________________________________________________________ diff --git a/kdecoration/breezesettingsdata.kcfg b/kdecoration/breezesettingsdata.kcfg --- a/kdecoration/breezesettingsdata.kcfg +++ b/kdecoration/breezesettingsdata.kcfg @@ -7,7 +7,7 @@ - 90 + 204 25 255