diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b307444..8aca0b09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,76 +1,74 @@ project(breeze) set(PROJECT_VERSION "5.14.80") set(PROJECT_VERSION_MAJOR 5) cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR) option(USE_KDE4 "Build a widget style for KDE4 (and nothing else)") include(GenerateExportHeader) include(WriteBasicConfigVersionFile) include(FeatureSummary) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules") - if(USE_KDE4) find_package(KDE4 REQUIRED) include(KDE4Defaults) include(MacroLibrary) add_definitions(${QT_DEFINITIONS} ${KDE4_DEFINITIONS}) include_directories(${KDE4_INCLUDES}) add_subdirectory(libbreezecommon) add_subdirectory(kstyle) else() find_package(ECM 0.0.9 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_SOURCE_DIR}/cmake) include(ECMInstallIcons) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(GtkUpdateIconCache) option(WITH_DECORATIONS "Build Breeze window decorations for KWin" ON) if(WITH_DECORATIONS) find_package(KDecoration2 REQUIRED) add_subdirectory(kdecoration) endif() add_subdirectory(colors) add_subdirectory(cursors) add_subdirectory(libbreezecommon) add_subdirectory(kstyle) add_subdirectory(misc) add_subdirectory(qtquickcontrols) add_subdirectory(wallpapers) find_package(KF5Package CONFIG REQUIRED) kpackage_install_package(lookandfeel.dark org.kde.breezedark.desktop look-and-feel plasma) if(EXISTS ${CMAKE_SOURCE_DIR}/po AND IS_DIRECTORY ${CMAKE_SOURCE_DIR}/po) find_package(KF5I18n CONFIG REQUIRED) ki18n_install(po) endif() include(ECMSetupVersion) ecm_setup_version(${PROJECT_VERSION} VARIABLE_PREFIX BREEZE PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/BreezeConfigVersion.cmake" ) # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${CMAKECONFIG_INSTALL_PREFIX}/Breeze") ecm_configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/BreezeConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/BreezeConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/BreezeConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/BreezeConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/cmake/Modules/FindFFTW.cmake b/cmake/Modules/FindFFTW.cmake deleted file mode 100644 index c3214373..00000000 --- a/cmake/Modules/FindFFTW.cmake +++ /dev/null @@ -1,20 +0,0 @@ -# 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/breezedecoration.cpp b/kdecoration/breezedecoration.cpp index e3f89df2..009867ad 100644 --- a/kdecoration/breezedecoration.cpp +++ b/kdecoration/breezedecoration.cpp @@ -1,836 +1,831 @@ /* * Copyright 2014 Martin Gräßlin * Copyright 2014 Hugo Pereira Da Costa +* Copyright 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 "breezedecoration.h" #include "breeze.h" #include "breezesettingsprovider.h" #include "config-breeze.h" #include "config/breezeconfigwidget.h" #include "breezebutton.h" #include "breezesizegrip.h" -#include "breezeboxshadowhelper.h" +#include "breezeboxshadowrenderer.h" #include #include #include #include #include #include #include #include #include #include #include #if BREEZE_HAVE_X11 #include #endif #include K_PLUGIN_FACTORY_WITH_JSON( BreezeDecoFactory, "breeze.json", registerPlugin(); registerPlugin(QStringLiteral("button")); registerPlugin(QStringLiteral("kcmodule")); ) namespace { struct ShadowParams { ShadowParams() : offset(QPoint(0, 0)) , radius(0) , opacity(0) {} ShadowParams(const QPoint &offset, int radius, qreal opacity) : offset(offset) , radius(radius) , opacity(opacity) {} QPoint offset; int radius; qreal opacity; }; struct CompositeShadowParams { CompositeShadowParams() = default; CompositeShadowParams( const QPoint &offset, const ShadowParams &shadow1, const ShadowParams &shadow2) : offset(offset) , shadow1(shadow1) , shadow2(shadow2) {} bool isNone() const { return qMax(shadow1.radius, shadow2.radius) == 0; } QPoint offset; ShadowParams shadow1; ShadowParams shadow2; }; const CompositeShadowParams s_shadowParams[] = { // None CompositeShadowParams(), // Small CompositeShadowParams( QPoint(0, 4), ShadowParams(QPoint(0, 0), 16, 0.6), ShadowParams(QPoint(0, -2), 4, 0.14)), // Medium CompositeShadowParams( QPoint(0, 8), ShadowParams(QPoint(0, 0), 32, 0.7), ShadowParams(QPoint(0, -5), 14, 0.12)), // Large CompositeShadowParams( QPoint(0, 18), ShadowParams(QPoint(0, 0), 64, 0.8), ShadowParams(QPoint(0, -10), 24, 0.1)), // Very large CompositeShadowParams( QPoint(0, 26), ShadowParams(QPoint(0, 0), 96, 0.95), ShadowParams(QPoint(0, -12), 28, 0.1)) }; inline CompositeShadowParams lookupShadowParams(int size) { switch (size) { case Breeze::InternalSettings::ShadowNone: return s_shadowParams[0]; case Breeze::InternalSettings::ShadowSmall: return s_shadowParams[1]; case Breeze::InternalSettings::ShadowMedium: return s_shadowParams[2]; case Breeze::InternalSettings::ShadowLarge: return s_shadowParams[3]; case Breeze::InternalSettings::ShadowVeryLarge: return s_shadowParams[4]; default: // Fallback to the Large size. return s_shadowParams[3]; } } } namespace Breeze { using KDecoration2::ColorRole; using KDecoration2::ColorGroup; //________________________________________________________________ static int g_sDecoCount = 0; static int g_shadowSizeEnum = InternalSettings::ShadowLarge; static int g_shadowStrength = 255; static QColor g_shadowColor = Qt::black; static QSharedPointer g_sShadow; //________________________________________________________________ Decoration::Decoration(QObject *parent, const QVariantList &args) : KDecoration2::Decoration(parent, args) , m_animation( new QPropertyAnimation( this ) ) { g_sDecoCount++; } //________________________________________________________________ Decoration::~Decoration() { g_sDecoCount--; if (g_sDecoCount == 0) { // last deco destroyed, clean up shadow g_sShadow.clear(); } deleteSizeGrip(); } //________________________________________________________________ void Decoration::setOpacity( qreal value ) { if( m_opacity == value ) return; m_opacity = value; update(); if( m_sizeGrip ) m_sizeGrip->update(); } //________________________________________________________________ QColor Decoration::titleBarColor() const { auto c = client().data(); if( hideTitleBar() ) return c->color( ColorGroup::Inactive, ColorRole::TitleBar ); else if( m_animation->state() == QPropertyAnimation::Running ) { return KColorUtils::mix( c->color( ColorGroup::Inactive, ColorRole::TitleBar ), c->color( ColorGroup::Active, ColorRole::TitleBar ), m_opacity ); } else return c->color( c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::TitleBar ); } //________________________________________________________________ QColor Decoration::outlineColor() const { auto c( client().data() ); if( !m_internalSettings->drawTitleBarSeparator() ) return QColor(); if( m_animation->state() == QPropertyAnimation::Running ) { QColor color( c->palette().color( QPalette::Highlight ) ); color.setAlpha( color.alpha()*m_opacity ); return color; } else if( c->isActive() ) return c->palette().color( QPalette::Highlight ); else return QColor(); } //________________________________________________________________ QColor Decoration::fontColor() const { auto c = client().data(); if( m_animation->state() == QPropertyAnimation::Running ) { return KColorUtils::mix( c->color( ColorGroup::Inactive, ColorRole::Foreground ), c->color( ColorGroup::Active, ColorRole::Foreground ), m_opacity ); } else return c->color( c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::Foreground ); } //________________________________________________________________ void Decoration::init() { auto c = client().data(); // active state change animation m_animation->setStartValue( 0 ); m_animation->setEndValue( 1.0 ); m_animation->setTargetObject( this ); m_animation->setPropertyName( "opacity" ); m_animation->setEasingCurve( QEasingCurve::InOutQuad ); reconfigure(); updateTitleBar(); auto s = settings(); connect(s.data(), &KDecoration2::DecorationSettings::borderSizeChanged, this, &Decoration::recalculateBorders); // a change in font might cause the borders to change connect(s.data(), &KDecoration2::DecorationSettings::fontChanged, this, &Decoration::recalculateBorders); connect(s.data(), &KDecoration2::DecorationSettings::spacingChanged, this, &Decoration::recalculateBorders); // buttons connect(s.data(), &KDecoration2::DecorationSettings::spacingChanged, this, &Decoration::updateButtonsGeometryDelayed); connect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsLeftChanged, this, &Decoration::updateButtonsGeometryDelayed); connect(s.data(), &KDecoration2::DecorationSettings::decorationButtonsRightChanged, this, &Decoration::updateButtonsGeometryDelayed); // full reconfiguration connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::reconfigure); connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, SettingsProvider::self(), &SettingsProvider::reconfigure, Qt::UniqueConnection ); connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::updateButtonsGeometryDelayed); connect(c, &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::maximizedHorizontallyChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::maximizedVerticallyChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::recalculateBorders); connect(c, &KDecoration2::DecoratedClient::captionChanged, this, [this]() { // update the caption area update(titleBar()); } ); connect(c, &KDecoration2::DecoratedClient::activeChanged, this, &Decoration::updateAnimationState); connect(c, &KDecoration2::DecoratedClient::widthChanged, this, &Decoration::updateTitleBar); connect(c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateTitleBar); connect(c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::setOpaque); connect(c, &KDecoration2::DecoratedClient::widthChanged, this, &Decoration::updateButtonsGeometry); connect(c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateButtonsGeometry); connect(c, &KDecoration2::DecoratedClient::adjacentScreenEdgesChanged, this, &Decoration::updateButtonsGeometry); connect(c, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateButtonsGeometry); createButtons(); createShadow(); } //________________________________________________________________ void Decoration::updateTitleBar() { auto s = settings(); auto c = client().data(); const bool maximized = isMaximized(); const int width = maximized ? c->width() : c->width() - 2*s->largeSpacing()*Metrics::TitleBar_SideMargin; const int height = maximized ? borderTop() : borderTop() - s->smallSpacing()*Metrics::TitleBar_TopMargin; const int x = maximized ? 0 : s->largeSpacing()*Metrics::TitleBar_SideMargin; const int y = maximized ? 0 : s->smallSpacing()*Metrics::TitleBar_TopMargin; setTitleBar(QRect(x, y, width, height)); } //________________________________________________________________ void Decoration::updateAnimationState() { if( m_internalSettings->animationsEnabled() ) { auto c = client().data(); m_animation->setDirection( c->isActive() ? QPropertyAnimation::Forward : QPropertyAnimation::Backward ); if( m_animation->state() != QPropertyAnimation::Running ) m_animation->start(); } else { update(); } } //________________________________________________________________ void Decoration::updateSizeGripVisibility() { auto c = client().data(); if( m_sizeGrip ) { m_sizeGrip->setVisible( c->isResizeable() && !isMaximized() && !c->isShaded() ); } } //________________________________________________________________ int Decoration::borderSize(bool bottom) const { const int baseSize = settings()->smallSpacing(); if( m_internalSettings && (m_internalSettings->mask() & BorderSize ) ) { switch (m_internalSettings->borderSize()) { case InternalSettings::BorderNone: return 0; case InternalSettings::BorderNoSides: return bottom ? qMax(4, baseSize) : 0; default: case InternalSettings::BorderTiny: return bottom ? qMax(4, baseSize) : baseSize; case InternalSettings::BorderNormal: return baseSize*2; case InternalSettings::BorderLarge: return baseSize*3; case InternalSettings::BorderVeryLarge: return baseSize*4; case InternalSettings::BorderHuge: return baseSize*5; case InternalSettings::BorderVeryHuge: return baseSize*6; case InternalSettings::BorderOversized: return baseSize*10; } } else { switch (settings()->borderSize()) { case KDecoration2::BorderSize::None: return 0; case KDecoration2::BorderSize::NoSides: return bottom ? qMax(4, baseSize) : 0; default: case KDecoration2::BorderSize::Tiny: return bottom ? qMax(4, baseSize) : baseSize; case KDecoration2::BorderSize::Normal: return baseSize*2; case KDecoration2::BorderSize::Large: return baseSize*3; case KDecoration2::BorderSize::VeryLarge: return baseSize*4; case KDecoration2::BorderSize::Huge: return baseSize*5; case KDecoration2::BorderSize::VeryHuge: return baseSize*6; case KDecoration2::BorderSize::Oversized: return baseSize*10; } } } //________________________________________________________________ void Decoration::reconfigure() { m_internalSettings = SettingsProvider::self()->internalSettings( this ); // animation m_animation->setDuration( m_internalSettings->animationsDuration() ); // borders recalculateBorders(); // shadow createShadow(); // size grip if( hasNoBorders() && m_internalSettings->drawSizeGrip() ) createSizeGrip(); else deleteSizeGrip(); } //________________________________________________________________ void Decoration::recalculateBorders() { auto c = client().data(); auto s = settings(); // left, right and bottom borders const int left = isLeftEdge() ? 0 : borderSize(); const int right = isRightEdge() ? 0 : borderSize(); const int bottom = (c->isShaded() || isBottomEdge()) ? 0 : borderSize(true); int top = 0; if( hideTitleBar() ) top = bottom; else { QFontMetrics fm(s->font()); top += qMax(fm.height(), buttonHeight() ); // padding below // extra pixel is used for the active window outline const int baseSize = s->smallSpacing(); top += baseSize*Metrics::TitleBar_BottomMargin + 1; // padding above top += baseSize*TitleBar_TopMargin; } setBorders(QMargins(left, top, right, bottom)); // extended sizes const int extSize = s->largeSpacing(); int extSides = 0; int extBottom = 0; if( hasNoBorders() ) { extSides = extSize; extBottom = extSize; } else if( hasNoSideBorders() ) { extSides = extSize; } setResizeOnlyBorders(QMargins(extSides, 0, extSides, extBottom)); } //________________________________________________________________ void Decoration::createButtons() { m_leftButtons = new KDecoration2::DecorationButtonGroup(KDecoration2::DecorationButtonGroup::Position::Left, this, &Button::create); m_rightButtons = new KDecoration2::DecorationButtonGroup(KDecoration2::DecorationButtonGroup::Position::Right, this, &Button::create); updateButtonsGeometry(); } //________________________________________________________________ void Decoration::updateButtonsGeometryDelayed() { QTimer::singleShot( 0, this, &Decoration::updateButtonsGeometry ); } //________________________________________________________________ void Decoration::updateButtonsGeometry() { const auto s = settings(); // adjust button position const int bHeight = captionHeight() + (isTopEdge() ? s->smallSpacing()*Metrics::TitleBar_TopMargin:0); const int bWidth = buttonHeight(); const int verticalOffset = (isTopEdge() ? s->smallSpacing()*Metrics::TitleBar_TopMargin:0) + (captionHeight()-buttonHeight())/2; foreach( const QPointer& button, m_leftButtons->buttons() + m_rightButtons->buttons() ) { button.data()->setGeometry( QRectF( QPoint( 0, 0 ), QSizeF( bWidth, bHeight ) ) ); static_cast( button.data() )->setOffset( QPointF( 0, verticalOffset ) ); static_cast( button.data() )->setIconSize( QSize( bWidth, bWidth ) ); } // left buttons if( !m_leftButtons->buttons().isEmpty() ) { // spacing m_leftButtons->setSpacing(s->smallSpacing()*Metrics::TitleBar_ButtonSpacing); // padding const int vPadding = isTopEdge() ? 0 : s->smallSpacing()*Metrics::TitleBar_TopMargin; const int hPadding = s->smallSpacing()*Metrics::TitleBar_SideMargin; if( isLeftEdge() ) { // add offsets on the side buttons, to preserve padding, but satisfy Fitts law auto button = static_cast( m_leftButtons->buttons().front().data() ); button->setGeometry( QRectF( QPoint( 0, 0 ), QSizeF( bWidth + hPadding, bHeight ) ) ); button->setFlag( Button::FlagFirstInList ); button->setHorizontalOffset( hPadding ); m_leftButtons->setPos(QPointF(0, vPadding)); } else m_leftButtons->setPos(QPointF(hPadding + borderLeft(), vPadding)); } // right buttons if( !m_rightButtons->buttons().isEmpty() ) { // spacing m_rightButtons->setSpacing(s->smallSpacing()*Metrics::TitleBar_ButtonSpacing); // padding const int vPadding = isTopEdge() ? 0 : s->smallSpacing()*Metrics::TitleBar_TopMargin; const int hPadding = s->smallSpacing()*Metrics::TitleBar_SideMargin; if( isRightEdge() ) { auto button = static_cast( m_rightButtons->buttons().back().data() ); button->setGeometry( QRectF( QPoint( 0, 0 ), QSizeF( bWidth + hPadding, bHeight ) ) ); button->setFlag( Button::FlagLastInList ); m_rightButtons->setPos(QPointF(size().width() - m_rightButtons->geometry().width(), vPadding)); } else m_rightButtons->setPos(QPointF(size().width() - m_rightButtons->geometry().width() - hPadding - borderRight(), vPadding)); } update(); } //________________________________________________________________ void Decoration::paint(QPainter *painter, const QRect &repaintRegion) { // TODO: optimize based on repaintRegion auto c = client().data(); auto s = settings(); // paint background if( !c->isShaded() ) { painter->fillRect(rect(), Qt::transparent); painter->save(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(Qt::NoPen); painter->setBrush( c->color( c->isActive() ? ColorGroup::Active : ColorGroup::Inactive, ColorRole::Frame ) ); // clip away the top part if( !hideTitleBar() ) painter->setClipRect(0, borderTop(), size().width(), size().height() - borderTop(), Qt::IntersectClip); if( s->isAlphaChannelSupported() ) painter->drawRoundedRect(rect(), Metrics::Frame_FrameRadius, Metrics::Frame_FrameRadius); else painter->drawRect( rect() ); painter->restore(); } if( !hideTitleBar() ) paintTitleBar(painter, repaintRegion); if( hasBorders() && !s->isAlphaChannelSupported() ) { painter->save(); painter->setRenderHint(QPainter::Antialiasing, false); painter->setBrush( Qt::NoBrush ); painter->setPen( c->isActive() ? c->color( ColorGroup::Active, ColorRole::TitleBar ): c->color( ColorGroup::Inactive, ColorRole::Foreground ) ); painter->drawRect( rect().adjusted( 0, 0, -1, -1 ) ); painter->restore(); } } //________________________________________________________________ void Decoration::paintTitleBar(QPainter *painter, const QRect &repaintRegion) { const auto c = client().data(); const QRect titleRect(QPoint(0, 0), QSize(size().width(), borderTop())); if ( !titleRect.intersects(repaintRegion) ) return; painter->save(); painter->setPen(Qt::NoPen); // render a linear gradient on title area if( c->isActive() && m_internalSettings->drawBackgroundGradient() ) { const QColor titleBarColor( this->titleBarColor() ); QLinearGradient gradient( 0, 0, 0, titleRect.height() ); gradient.setColorAt(0.0, titleBarColor.lighter( 120 ) ); gradient.setColorAt(0.8, titleBarColor); painter->setBrush(gradient); } else { painter->setBrush( titleBarColor() ); } auto s = settings(); if( isMaximized() || !s->isAlphaChannelSupported() ) { painter->drawRect(titleRect); } else if( c->isShaded() ) { painter->drawRoundedRect(titleRect, Metrics::Frame_FrameRadius, Metrics::Frame_FrameRadius); } else { painter->setClipRect(titleRect, Qt::IntersectClip); // the rect is made a little bit larger to be able to clip away the rounded corners at the bottom and sides painter->drawRoundedRect(titleRect.adjusted( isLeftEdge() ? -Metrics::Frame_FrameRadius:0, isTopEdge() ? -Metrics::Frame_FrameRadius:0, isRightEdge() ? Metrics::Frame_FrameRadius:0, Metrics::Frame_FrameRadius), Metrics::Frame_FrameRadius, Metrics::Frame_FrameRadius); } const QColor outlineColor( this->outlineColor() ); if( !c->isShaded() && outlineColor.isValid() ) { // outline painter->setRenderHint( QPainter::Antialiasing, false ); painter->setBrush( Qt::NoBrush ); painter->setPen( outlineColor ); painter->drawLine( titleRect.bottomLeft(), titleRect.bottomRight() ); } painter->restore(); // draw caption painter->setFont(s->font()); painter->setPen( fontColor() ); const auto cR = captionRect(); const QString caption = painter->fontMetrics().elidedText(c->caption(), Qt::ElideMiddle, cR.first.width()); painter->drawText(cR.first, cR.second | Qt::TextSingleLine, caption); // draw all buttons m_leftButtons->paint(painter, repaintRegion); m_rightButtons->paint(painter, repaintRegion); } //________________________________________________________________ int Decoration::buttonHeight() const { const int baseSize = settings()->gridUnit(); switch( m_internalSettings->buttonSize() ) { case InternalSettings::ButtonTiny: return baseSize; case InternalSettings::ButtonSmall: return baseSize*1.5; default: case InternalSettings::ButtonDefault: return baseSize*2; case InternalSettings::ButtonLarge: return baseSize*2.5; case InternalSettings::ButtonVeryLarge: return baseSize*3.5; } } //________________________________________________________________ int Decoration::captionHeight() const { return hideTitleBar() ? borderTop() : borderTop() - settings()->smallSpacing()*(Metrics::TitleBar_BottomMargin + Metrics::TitleBar_TopMargin ) - 1; } //________________________________________________________________ QPair Decoration::captionRect() const { if( hideTitleBar() ) return qMakePair( QRect(), Qt::AlignCenter ); else { auto c = client().data(); const int leftOffset = m_leftButtons->buttons().isEmpty() ? Metrics::TitleBar_SideMargin*settings()->smallSpacing(): m_leftButtons->geometry().x() + m_leftButtons->geometry().width() + Metrics::TitleBar_SideMargin*settings()->smallSpacing(); const int rightOffset = m_rightButtons->buttons().isEmpty() ? Metrics::TitleBar_SideMargin*settings()->smallSpacing() : size().width() - m_rightButtons->geometry().x() + Metrics::TitleBar_SideMargin*settings()->smallSpacing(); const int yOffset = settings()->smallSpacing()*Metrics::TitleBar_TopMargin; const QRect maxRect( leftOffset, yOffset, size().width() - leftOffset - rightOffset, captionHeight() ); switch( m_internalSettings->titleAlignment() ) { case InternalSettings::AlignLeft: return qMakePair( maxRect, Qt::AlignVCenter|Qt::AlignLeft ); case InternalSettings::AlignRight: return qMakePair( maxRect, Qt::AlignVCenter|Qt::AlignRight ); case InternalSettings::AlignCenter: return qMakePair( maxRect, Qt::AlignCenter ); default: case InternalSettings::AlignCenterFullWidth: { // full caption rect const QRect fullRect = QRect( 0, yOffset, size().width(), captionHeight() ); QRect boundingRect( settings()->fontMetrics().boundingRect( c->caption()).toRect() ); // text bounding rect boundingRect.setTop( yOffset ); boundingRect.setHeight( captionHeight() ); boundingRect.moveLeft( ( size().width() - boundingRect.width() )/2 ); if( boundingRect.left() < leftOffset ) return qMakePair( maxRect, Qt::AlignVCenter|Qt::AlignLeft ); else if( boundingRect.right() > size().width() - rightOffset ) return qMakePair( maxRect, Qt::AlignVCenter|Qt::AlignRight ); else return qMakePair(fullRect, Qt::AlignCenter); } } } } //________________________________________________________________ void Decoration::createShadow() { if (!g_sShadow ||g_shadowSizeEnum != m_internalSettings->shadowSize() || g_shadowStrength != m_internalSettings->shadowStrength() || g_shadowColor != m_internalSettings->shadowColor()) { g_shadowSizeEnum = m_internalSettings->shadowSize(); g_shadowStrength = m_internalSettings->shadowStrength(); g_shadowColor = m_internalSettings->shadowColor(); const CompositeShadowParams params = lookupShadowParams(g_shadowSizeEnum); if (params.isNone()) { g_sShadow.clear(); setShadow(g_sShadow); return; } auto withOpacity = [](const QColor &color, qreal opacity) -> QColor { QColor c(color); c.setAlphaF(opacity); return c; }; - // In order to properly render a box shadow with a given radius `shadowSize`, - // the box size should be at least `2 * QSize(shadowSize, shadowSize)`. - const int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); - const QRect box(shadowSize, shadowSize, 2 * shadowSize + 1, 2 * shadowSize + 1); - const QRect rect = box.adjusted(-shadowSize, -shadowSize, shadowSize, shadowSize); + const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); - QImage shadow(rect.size(), QImage::Format_ARGB32_Premultiplied); - shadow.fill(Qt::transparent); - - QPainter painter(&shadow); - painter.setRenderHint(QPainter::Antialiasing); + BoxShadowRenderer shadowRenderer; + shadowRenderer.setBorderRadius(Metrics::Frame_FrameRadius + 0.5); + shadowRenderer.setBoxSize(boxSize); + shadowRenderer.setDevicePixelRatio(1.0); // TODO: Create HiDPI shadows? const qreal strength = static_cast(g_shadowStrength) / 255.0; - - // Draw the "shape" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow1.offset, - params.shadow1.radius, + shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, withOpacity(g_shadowColor, params.shadow1.opacity * strength)); - - // Draw the "contrast" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow2.offset, - params.shadow2.radius, + shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, withOpacity(g_shadowColor, params.shadow2.opacity * strength)); + QImage shadowTexture = shadowRenderer.render(); + + QPainter painter(&shadowTexture); + painter.setRenderHint(QPainter::Antialiasing); + + const QRect outerRect = shadowTexture.rect(); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(outerRect.center()); + // Mask out inner rect. const QMargins padding = QMargins( - shadowSize - Metrics::Shadow_Overlap - params.offset.x(), - shadowSize - Metrics::Shadow_Overlap - params.offset.y(), - shadowSize - Metrics::Shadow_Overlap + params.offset.x(), - shadowSize - Metrics::Shadow_Overlap + params.offset.y()); - const QRect innerRect = rect - padding; + boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(), + boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(), + outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(), + outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y()); + const QRect innerRect = outerRect - padding; painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); painter.drawRoundedRect( innerRect, Metrics::Frame_FrameRadius + 0.5, Metrics::Frame_FrameRadius + 0.5); // Draw outline. painter.setPen(withOpacity(g_shadowColor, 0.2 * strength)); painter.setBrush(Qt::NoBrush); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); painter.drawRoundedRect( innerRect, Metrics::Frame_FrameRadius - 0.5, Metrics::Frame_FrameRadius - 0.5); painter.end(); g_sShadow = QSharedPointer::create(); g_sShadow->setPadding(padding); - g_sShadow->setInnerShadowRect(QRect(shadow.rect().center(), QSize(1, 1))); - g_sShadow->setShadow(shadow); + g_sShadow->setInnerShadowRect(QRect(outerRect.center(), QSize(1, 1))); + g_sShadow->setShadow(shadowTexture); } setShadow(g_sShadow); } //_________________________________________________________________ void Decoration::createSizeGrip() { // do nothing if size grip already exist if( m_sizeGrip ) return; #if BREEZE_HAVE_X11 if( !QX11Info::isPlatformX11() ) return; // access client auto c = client().data(); if( !c ) return; if( c->windowId() != 0 ) { m_sizeGrip = new SizeGrip( this ); connect( c, &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateSizeGripVisibility ); connect( c, &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateSizeGripVisibility ); connect( c, &KDecoration2::DecoratedClient::resizeableChanged, this, &Decoration::updateSizeGripVisibility ); } #endif } //_________________________________________________________________ void Decoration::deleteSizeGrip() { if( m_sizeGrip ) { m_sizeGrip->deleteLater(); m_sizeGrip = nullptr; } } } // namespace #include "breezedecoration.moc" diff --git a/kstyle/breezemdiwindowshadow.cpp b/kstyle/breezemdiwindowshadow.cpp index 560bd2ea..95fde212 100644 --- a/kstyle/breezemdiwindowshadow.cpp +++ b/kstyle/breezemdiwindowshadow.cpp @@ -1,253 +1,263 @@ /************************************************************************* * Copyright (C) 2014 by Hugo Pereira Da Costa * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * *************************************************************************/ #include "breezemdiwindowshadow.h" #include "breeze.h" +#include "breezeboxshadowrenderer.h" #include "breezeshadowhelper.h" #include "breezestyleconfigdata.h" #include #include #include #include namespace Breeze { //____________________________________________________________________ MdiWindowShadow::MdiWindowShadow( QWidget* parent, TileSet shadowTiles ): QWidget( parent ), _shadowTiles( shadowTiles ) { setAttribute( Qt::WA_OpaquePaintEvent, false ); setAttribute( Qt::WA_TransparentForMouseEvents, true ); setFocusPolicy( Qt::NoFocus ); } //____________________________________________________________________ void MdiWindowShadow::updateGeometry() { if( !_widget ) return; // metrics const CompositeShadowParams params = ShadowHelper::lookupShadowParams( StyleConfigData::shadowSize() ); if( params.isNone() ) return; - const int shadowSize = qMax( params.shadow1.radius, params.shadow2.radius ); - const int size( shadowSize - Metrics::Shadow_Overlap ); - const int topSize( size - params.offset.y() ); - const int bottomSize( size + params.offset.y() ); - const int leftSize( size - params.offset.x() ); - const int rightSize( size + params.offset.x() ); + const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); + + const QSize shadowSize = BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow1.radius, params.shadow1.offset) + .expandedTo(BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow2.radius, params.shadow2.offset)); + + const QRect shadowRect(QPoint(0, 0), shadowSize); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(shadowRect.center()); + + const int topSize( boxRect.top() - shadowRect.top() - Metrics::Shadow_Overlap - params.offset.y() ); + const int bottomSize( shadowRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y() ); + const int leftSize( boxRect.left() - shadowRect.left() - Metrics::Shadow_Overlap - params.offset.x() ); + const int rightSize( shadowRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x() ); // get tileSet rect auto hole = _widget->frameGeometry(); _shadowTilesRect = hole.adjusted( -leftSize, -topSize, rightSize, bottomSize ); // get parent MDI area's viewport auto parent( parentWidget() ); if (parent && !qobject_cast(parent) && qobject_cast(parent->parentWidget())) { parent = parent->parentWidget(); } if( qobject_cast( parent ) ) { parent = qobject_cast( parent )->viewport(); } // set geometry QRect geometry( _shadowTilesRect ); if( parent ) { geometry &= parent->rect(); hole &= parent->rect(); } // update geometry and mask const QRegion mask = QRegion( geometry ) - hole.adjusted( 2, 2, -2, -2 ); if( mask.isEmpty() ) hide(); else { setGeometry( geometry ); setMask( mask.translated( -geometry.topLeft() ) ); show(); } // translate rendering rect _shadowTilesRect.translate( -geometry.topLeft() ); } //____________________________________________________________________ void MdiWindowShadow::updateZOrder() { stackUnder( _widget ); } //____________________________________________________________________ void MdiWindowShadow::paintEvent( QPaintEvent* event ) { if( !_shadowTiles.isValid() ) return; QPainter painter( this ); painter.setRenderHints( QPainter::Antialiasing ); painter.setClipRegion( event->region() ); _shadowTiles.render( _shadowTilesRect, &painter ); } //____________________________________________________________________ MdiWindowShadowFactory::MdiWindowShadowFactory( QObject* parent ): QObject( parent ) {} //____________________________________________________________________________________ bool MdiWindowShadowFactory::registerWidget( QWidget* widget ) { // check widget type auto subwindow( qobject_cast( widget ) ); if( !subwindow ) return false; if( subwindow->widget() && subwindow->widget()->inherits( "KMainWindow" ) ) return false; // make sure widget is not already registered if( isRegistered( widget ) ) return false; // store in set _registeredWidgets.insert( widget ); // create shadow immediatly if widget is already visible if( widget->isVisible() ) { installShadow( widget ); updateShadowGeometry( widget ); updateShadowZOrder( widget ); } widget->installEventFilter( this ); // catch object destruction connect( widget, SIGNAL(destroyed(QObject*)), SLOT(widgetDestroyed(QObject*)) ); return true; } //____________________________________________________________________________________ void MdiWindowShadowFactory::unregisterWidget( QWidget* widget ) { if( !isRegistered( widget ) ) return; widget->removeEventFilter( this ); _registeredWidgets.remove( widget ); removeShadow( widget ); } //____________________________________________________________________________________ bool MdiWindowShadowFactory::eventFilter( QObject* object, QEvent* event ) { switch( event->type() ) { // TODO: possibly implement ZOrderChange event, to make sure that // the shadow is always painted on top case QEvent::ZOrderChange: updateShadowZOrder( object ); break; case QEvent::Destroy: if( isRegistered( object ) ) { _registeredWidgets.remove( object ); removeShadow( object ); } break; case QEvent::Hide: hideShadows( object ); break; case QEvent::Show: installShadow( object ); updateShadowGeometry( object ); updateShadowZOrder( object ); break; case QEvent::Move: case QEvent::Resize: updateShadowGeometry( object ); break; default: break; } return QObject::eventFilter( object, event ); } //____________________________________________________________________________________ MdiWindowShadow* MdiWindowShadowFactory::findShadow( QObject* object ) const { // check object, if( !object->parent() ) return nullptr; // find existing window shadows auto children = object->parent()->children(); foreach( QObject *child, children ) { if( MdiWindowShadow* shadow = qobject_cast(child) ) { if( shadow->widget() == object ) return shadow; } } return nullptr; } //____________________________________________________________________________________ void MdiWindowShadowFactory::installShadow( QObject* object ) { // cast auto widget( static_cast( object ) ); if( !widget->parentWidget() ) return; // make sure shadow is not already installed if( findShadow( object ) ) return; if ( !_shadowHelper ) return; // create new shadow auto windowShadow( new MdiWindowShadow( widget->parentWidget(), _shadowHelper->shadowTiles() ) ); windowShadow->setWidget( widget ); } //____________________________________________________________________________________ void MdiWindowShadowFactory::removeShadow( QObject* object ) { if( MdiWindowShadow* windowShadow = findShadow( object ) ) { windowShadow->hide(); windowShadow->deleteLater(); } } //____________________________________________________________________________________ void MdiWindowShadowFactory::widgetDestroyed( QObject* object ) { _registeredWidgets.remove( object ); } } diff --git a/kstyle/breezeshadowhelper.cpp b/kstyle/breezeshadowhelper.cpp index 114b11df..582d1a41 100644 --- a/kstyle/breezeshadowhelper.cpp +++ b/kstyle/breezeshadowhelper.cpp @@ -1,658 +1,669 @@ /************************************************************************* * Copyright (C) 2014 by Hugo Pereira Da Costa * + * Copyright (C) 2018 by 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * *************************************************************************/ #include "breezeshadowhelper.h" #include "breeze.h" -#include "breezeboxshadowhelper.h" +#include "breezeboxshadowrenderer.h" #include "breezehelper.h" #include "breezepropertynames.h" #include "breezestyleconfigdata.h" #include #include #include #include #include #include #include #include #if BREEZE_HAVE_X11 #include #endif #if BREEZE_HAVE_KWAYLAND #include #include #include #include #include #include #endif namespace { using Breeze::CompositeShadowParams; using Breeze::ShadowParams; const CompositeShadowParams s_shadowParams[] = { // None CompositeShadowParams(), // Small CompositeShadowParams( QPoint(0, 6), ShadowParams(QPoint(0, 0), 12, 0.2), ShadowParams(QPoint(0, -3), 6, 0.16)), // Medium CompositeShadowParams( QPoint(0, 8), ShadowParams(QPoint(0, 0), 16, 0.21), ShadowParams(QPoint(0, -4), 6, 0.14)), // Large CompositeShadowParams( QPoint(0, 10), ShadowParams(QPoint(0, 0), 20, 0.23), ShadowParams(QPoint(0, -5), 8, 0.12)), // Very Large CompositeShadowParams( QPoint(0, 12), ShadowParams(QPoint(0, 0), 24, 0.26), ShadowParams(QPoint(0, -5), 10, 0.12)) }; } namespace Breeze { const char ShadowHelper::netWMShadowAtomName[] ="_KDE_NET_WM_SHADOW"; //_____________________________________________________ CompositeShadowParams ShadowHelper::lookupShadowParams(int shadowSizeEnum) { switch (shadowSizeEnum) { case StyleConfigData::ShadowNone: return s_shadowParams[0]; case StyleConfigData::ShadowSmall: return s_shadowParams[1]; case StyleConfigData::ShadowMedium: return s_shadowParams[2]; case StyleConfigData::ShadowLarge: return s_shadowParams[3]; case StyleConfigData::ShadowVeryLarge: return s_shadowParams[4]; default: // Fallback to the Large size. return s_shadowParams[3]; } } //_____________________________________________________ ShadowHelper::ShadowHelper( QObject* parent, Helper& helper ): QObject( parent ), _helper( helper ) #if BREEZE_HAVE_X11 ,_gc( 0 ), _atom( 0 ) #endif #if BREEZE_HAVE_KWAYLAND , _shadowManager( nullptr ) , _shmPool( nullptr ) #endif { // delay till event dispatcher is running as Wayland is highly async QMetaObject::invokeMethod(this, "initializeWayland", Qt::QueuedConnection); } //_______________________________________________________ ShadowHelper::~ShadowHelper() { #if BREEZE_HAVE_X11 if( Helper::isX11() ) { foreach( const quint32& value, _pixmaps ) xcb_free_pixmap( Helper::connection(), value ); } #endif } //_______________________________________________________ void ShadowHelper::initializeWayland() { #if BREEZE_HAVE_KWAYLAND if( !Helper::isWayland() ) return; using namespace KWayland::Client; auto connection = ConnectionThread::fromApplication( this ); if( !connection ) { return; } auto registry = new Registry( connection ); registry->create( connection ); connect(registry, &Registry::interfacesAnnounced, this, [registry, this] { const auto interface = registry->interface( Registry::Interface::Shadow ); if( interface.name != 0 ) { _shadowManager = registry->createShadowManager( interface.name, interface.version, registry ); } const auto shmInterface = registry->interface( Registry::Interface::Shm ); if( shmInterface.name != 0 ) { _shmPool = registry->createShmPool( shmInterface.name, shmInterface.version, registry ); } } ); registry->setup(); connection->roundtrip(); #endif } //______________________________________________ void ShadowHelper::reset() { #if BREEZE_HAVE_X11 if( Helper::isX11() ) { foreach( const quint32& value, _pixmaps ) xcb_free_pixmap( Helper::connection(), value ); } #endif _pixmaps.clear(); _shadowTiles = TileSet(); } //_______________________________________________________ bool ShadowHelper::registerWidget( QWidget* widget, bool force ) { // make sure widget is not already registered if( _widgets.contains( widget ) ) return false; // check if widget qualifies if( !( force || acceptWidget( widget ) ) ) { return false; } // try create shadow directly if( installShadows( widget ) ) _widgets.insert( widget, widget->winId() ); else _widgets.insert( widget, 0 ); // install event filter widget->removeEventFilter( this ); widget->installEventFilter( this ); // connect destroy signal connect( widget, SIGNAL(destroyed(QObject*)), SLOT(objectDeleted(QObject*)) ); return true; } //_______________________________________________________ void ShadowHelper::unregisterWidget( QWidget* widget ) { if( _widgets.remove( widget ) ) { uninstallShadows( widget ); } } //_______________________________________________________ void ShadowHelper::loadConfig() { // reset reset(); // update property for registered widgets for( QMap::const_iterator iter = _widgets.constBegin(); iter != _widgets.constEnd(); ++iter ) { installShadows( iter.key() ); } } //_______________________________________________________ bool ShadowHelper::eventFilter( QObject* object, QEvent* event ) { if( Helper::isWayland() ) { #if BREEZE_HAVE_KWAYLAND QWidget* widget( static_cast( object ) ); if( event->type() == QEvent::Paint ) { auto iter = _widgetSurfaces.constFind( widget ); if( iter == _widgetSurfaces.constEnd() ) { // install shadows and update winId installShadows( widget ); } } else if( event->type() == QEvent::Hide ) { auto iter = _widgetSurfaces.find( widget ); if( iter != _widgetSurfaces.end() ) { _widgetSurfaces.erase( iter ); } } #endif } else if( Helper::isX11() ) { // check event type if( event->type() != QEvent::WinIdChange ) return false; // cast widget QWidget* widget( static_cast( object ) ); // install shadows and update winId if( installShadows( widget ) ) { _widgets.insert( widget, widget->winId() ); } } return false; } //_______________________________________________________ TileSet ShadowHelper::shadowTiles() { const CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize()); if (params.isNone()) { return TileSet(); } else if (_shadowTiles.isValid()) { return _shadowTiles; } auto withOpacity = [](const QColor &color, qreal opacity) -> QColor { QColor c(color); c.setAlphaF(opacity); return c; }; - const int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); const QColor color = StyleConfigData::shadowColor(); const qreal strength = static_cast(StyleConfigData::shadowStrength()) / 255.0; - const QRect box( - shadowSize, - shadowSize, - 2 * shadowSize + 1, - 2 * shadowSize + 1); - const QRect outerRect = box.adjusted(-shadowSize, -shadowSize, shadowSize, shadowSize); + const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); - QPixmap shadow = _helper.highDpiPixmap(outerRect.width(), outerRect.height()); - shadow.fill(Qt::transparent); + #if QT_VERSION >= 0x050300 + const qreal dpr = qApp->devicePixelRatio(); + #else + const qreal dpr = 1.0; + #endif - QPainter painter(&shadow); - painter.setRenderHint(QPainter::Antialiasing); + const qreal frameRadius = _helper.frameRadius(); - // Draw the "shape" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow1.offset, - params.shadow1.radius, - withOpacity(color, params.shadow1.opacity * strength)); + BoxShadowRenderer shadowRenderer; + shadowRenderer.setBorderRadius(frameRadius); + shadowRenderer.setBoxSize(boxSize); + shadowRenderer.setDevicePixelRatio(dpr); - // Draw the "contrast" shadow. - BoxShadowHelper::boxShadow( - &painter, - box, - params.shadow2.offset, - params.shadow2.radius, + shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, + withOpacity(color, params.shadow1.opacity * strength)); + shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, withOpacity(color, params.shadow2.opacity * strength)); + QImage shadowTexture = shadowRenderer.render(); + + const QRect outerRect(QPoint(0, 0), shadowTexture.size() / dpr); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(outerRect.center()); + // Mask out inner rect. + QPainter painter(&shadowTexture); + painter.setRenderHint(QPainter::Antialiasing); + const QMargins margins = QMargins( - shadowSize - Metrics::Shadow_Overlap - params.offset.x(), - shadowSize - Metrics::Shadow_Overlap - params.offset.y(), - shadowSize - Metrics::Shadow_Overlap + params.offset.x(), - shadowSize - Metrics::Shadow_Overlap + params.offset.y()); - const qreal frameRadius = _helper.frameRadius(); + boxRect.left() - outerRect.left() - Metrics::Shadow_Overlap - params.offset.x(), + boxRect.top() - outerRect.top() - Metrics::Shadow_Overlap - params.offset.y(), + outerRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(), + outerRect.bottom() - boxRect.bottom() - Metrics::Shadow_Overlap + params.offset.y()); painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); painter.drawRoundedRect( #if BREEZE_USE_KDE4 outerRect.adjusted(margins.left(), margins.top(), -margins.right(), -margins.bottom()), #else outerRect - margins, #endif frameRadius, frameRadius); // We're done. painter.end(); const QPoint innerRectTopLeft = outerRect.center(); _shadowTiles = TileSet( - shadow, + QPixmap::fromImage(shadowTexture), innerRectTopLeft.x(), innerRectTopLeft.y(), 1, 1); return _shadowTiles; } //_______________________________________________________ void ShadowHelper::objectDeleted( QObject* object ) { _widgets.remove( static_cast( object ) ); } //_______________________________________________________ bool ShadowHelper::isMenu( QWidget* widget ) const { return qobject_cast( widget ); } //_______________________________________________________ bool ShadowHelper::isToolTip( QWidget* widget ) const { return widget->inherits( "QTipLabel" ) || (widget->windowFlags() & Qt::WindowType_Mask) == Qt::ToolTip; } //_______________________________________________________ bool ShadowHelper::isDockWidget( QWidget* widget ) const { return qobject_cast( widget ); } //_______________________________________________________ bool ShadowHelper::isToolBar( QWidget* widget ) const { return qobject_cast( widget ); } //_______________________________________________________ bool ShadowHelper::acceptWidget( QWidget* widget ) const { // flags if( widget->property( PropertyNames::netWMSkipShadow ).toBool() ) return false; if( widget->property( PropertyNames::netWMForceShadow ).toBool() ) return true; // menus if( isMenu( widget ) ) return true; // combobox dropdown lists if( widget->inherits( "QComboBoxPrivateContainer" ) ) return true; // tooltips if( isToolTip( widget ) && !widget->inherits( "Plasma::ToolTip" ) ) { return true; } // detached widgets if( isDockWidget( widget ) || isToolBar( widget ) ) { return true; } // reject return false; } //______________________________________________ const QVector& ShadowHelper::createPixmapHandles() { /** shadow atom and property specification available at http://community.kde.org/KWin/Shadow */ // create atom #if BREEZE_HAVE_X11 if( !_atom && Helper::isX11() ) _atom = _helper.createAtom( QLatin1String( netWMShadowAtomName ) ); #endif // make sure size is valid if( _pixmaps.empty() ) { _pixmaps = QVector { createPixmap( _shadowTiles.pixmap( 1 ) ), createPixmap( _shadowTiles.pixmap( 2 ) ), createPixmap( _shadowTiles.pixmap( 5 ) ), createPixmap( _shadowTiles.pixmap( 8 ) ), createPixmap( _shadowTiles.pixmap( 7 ) ), createPixmap( _shadowTiles.pixmap( 6 ) ), createPixmap( _shadowTiles.pixmap( 3 ) ), createPixmap( _shadowTiles.pixmap( 0 ) ) }; } // return relevant list of pixmap handles return _pixmaps; } //______________________________________________ quint32 ShadowHelper::createPixmap( const QPixmap& source ) { // do nothing for invalid pixmaps if( source.isNull() ) return 0; if( !Helper::isX11() ) return 0; /* in some cases, pixmap handle is invalid. This is the case notably when Qt uses to RasterEngine. In this case, we create an X11 Pixmap explicitly and draw the source pixmap on it. */ #if BREEZE_HAVE_X11 const int width( source.width() ); const int height( source.height() ); // create X11 pixmap xcb_pixmap_t pixmap = xcb_generate_id( Helper::connection() ); xcb_create_pixmap( Helper::connection(), 32, pixmap, QX11Info::appRootWindow(), width, height ); // create gc if( !_gc ) { _gc = xcb_generate_id( Helper::connection() ); xcb_create_gc( Helper::connection(), _gc, pixmap, 0, nullptr ); } // create image from QPixmap and assign to pixmap QImage image( source.toImage() ); xcb_put_image( Helper::connection(), XCB_IMAGE_FORMAT_Z_PIXMAP, pixmap, _gc, image.width(), image.height(), 0, 0, 0, 32, image.byteCount(), image.constBits()); return pixmap; #else return 0; #endif } //_______________________________________________________ bool ShadowHelper::installShadows( QWidget* widget ) { if( !widget ) return false; /* From bespin code. Supposibly prevent playing with some 'pseudo-widgets' that have winId matching some other -random- window */ if( !(widget->testAttribute(Qt::WA_WState_Created) && widget->internalWinId() )) { return false; } // create shadow tiles if needed shadowTiles(); if( !_shadowTiles.isValid() ) return false; if( Helper::isX11() ) return installX11Shadows( widget ); if( Helper::isWayland() ) return installWaylandShadows( widget ); return false; } //_______________________________________________________ bool ShadowHelper::installX11Shadows( QWidget* widget ) { #if BREEZE_HAVE_X11 #ifndef QT_NO_XRENDER // create pixmap handles if needed QVector data( createPixmapHandles() ); if( data.size() != numPixmaps ) return false; const QMargins margins = shadowMargins( widget ); const quint32 topSize = margins.top(); const quint32 bottomSize = margins.bottom(); const quint32 leftSize( margins.left() ); const quint32 rightSize( margins.right() ); // assign to data and xcb property data << QVector{topSize, rightSize, bottomSize, leftSize}; xcb_change_property( Helper::connection(), XCB_PROP_MODE_REPLACE, widget->winId(), _atom, XCB_ATOM_CARDINAL, 32, data.size(), data.constData() ); xcb_flush( Helper::connection() ); return true; #endif #endif return false; } //_______________________________________________________ bool ShadowHelper::installWaylandShadows( QWidget* widget ) { #if BREEZE_HAVE_KWAYLAND if( widget->windowHandle()->parent() ) return false; if( !_shadowManager || !_shmPool ) return false; // create shadow using namespace KWayland::Client; auto s = Surface::fromWindow( widget->windowHandle() ); if( !s ) return false; auto shadow = _shadowManager->createShadow( s, widget ); if( !shadow->isValid() ) return false; // add the shadow elements shadow->attachTop( _shmPool->createBuffer( _shadowTiles.pixmap( 1 ).toImage() ) ); shadow->attachTopRight( _shmPool->createBuffer( _shadowTiles.pixmap( 2 ).toImage() ) ); shadow->attachRight( _shmPool->createBuffer( _shadowTiles.pixmap( 5 ).toImage() ) ); shadow->attachBottomRight( _shmPool->createBuffer( _shadowTiles.pixmap( 8 ).toImage() ) ); shadow->attachBottom( _shmPool->createBuffer( _shadowTiles.pixmap( 7 ).toImage() ) ); shadow->attachBottomLeft( _shmPool->createBuffer( _shadowTiles.pixmap( 6 ).toImage() ) ); shadow->attachLeft( _shmPool->createBuffer( _shadowTiles.pixmap( 3 ).toImage() ) ); shadow->attachTopLeft( _shmPool->createBuffer( _shadowTiles.pixmap( 0 ).toImage() ) ); shadow->setOffsets( shadowMargins( widget ) ); shadow->commit(); s->commit( Surface::CommitFlag::None ); _widgetSurfaces.insert(widget, s); return true; #else Q_UNUSED( widget ); #endif return false; } //_______________________________________________________ QMargins ShadowHelper::shadowMargins( QWidget* widget ) const { const CompositeShadowParams params = lookupShadowParams(StyleConfigData::shadowSize()); if (params.isNone()) { return QMargins(); } - const int shadowSize = qMax(params.shadow1.radius, params.shadow2.radius); + const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); + + const QSize shadowSize = BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow1.radius, params.shadow1.offset) + .expandedTo(BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow2.radius, params.shadow2.offset)); + + const QRect shadowRect(QPoint(0, 0), shadowSize); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(shadowRect.center()); + QMargins margins( - shadowSize - Metrics::Shadow_Overlap - params.offset.x(), - shadowSize - Metrics::Shadow_Overlap - params.offset.y(), - shadowSize - Metrics::Shadow_Overlap + params.offset.x(), - shadowSize - Metrics::Shadow_Overlap + params.offset.y()); + boxRect.left() - shadowRect.left() - Metrics::Shadow_Overlap - params.offset.x(), + boxRect.top() - shadowRect.top() - Metrics::Shadow_Overlap - params.offset.y(), + shadowRect.right() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.x(), + shadowRect.bottom() - boxRect.right() - Metrics::Shadow_Overlap + params.offset.y()); if (widget->inherits("QBalloonTip")) { // Balloon tip needs special margins to deal with the arrow. int top = 0; int bottom = 0; widget->getContentsMargins(nullptr, &top, nullptr, &bottom); // Need to decrement default size further due to extra hard coded round corner. #if BREEZE_USE_KDE4 margins.setLeft(margins.left() - 1); margins.setTop(margins.top() - 1); margins.setRight(margins.right() - 1); margins.setBottom(margins.bottom() - 1); #else margins -= 1; #endif // Arrow can be either to the top or the bottom. Adjust margins accordingly. const int diff = qAbs(top - bottom); if (top > bottom) { margins.setTop(margins.top() - diff); } else { margins.setBottom(margins.bottom() - diff); } } #if BREEZE_USE_KDE4 const qreal dpr = _helper.devicePixelRatio(_shadowTiles.pixmap(0)); margins.setLeft(margins.left() * dpr); margins.setTop(margins.top() * dpr); margins.setRight(margins.right() * dpr); margins.setBottom(margins.bottom() * dpr); #else margins *= _helper.devicePixelRatio(_shadowTiles.pixmap(0)); #endif return margins; } //_______________________________________________________ void ShadowHelper::uninstallShadows( QWidget* widget ) const { if( !( widget && widget->testAttribute(Qt::WA_WState_Created) ) ) return; if( Helper::isX11() ) uninstallX11Shadows( widget ); if( Helper::isWayland() ) uninstallWaylandShadows( widget ); } //_______________________________________________________ void ShadowHelper::uninstallX11Shadows( QWidget* widget ) const { #if BREEZE_HAVE_X11 xcb_delete_property( Helper::connection(), widget->winId(), _atom); #else Q_UNUSED( widget ) #endif } //_______________________________________________________ void ShadowHelper::uninstallWaylandShadows( QWidget* widget ) const { #if BREEZE_HAVE_KWAYLAND if( widget->windowHandle() && widget->windowHandle()->parent() ) return; if( !_shadowManager ) return; using namespace KWayland::Client; auto s = Surface::fromWindow( widget->windowHandle() ); if( !s ) return; _shadowManager->removeShadow( s ); s->commit( Surface::CommitFlag::None ); #else Q_UNUSED( widget ) #endif } } diff --git a/libbreezecommon/CMakeLists.txt b/libbreezecommon/CMakeLists.txt index 92d924b0..2717542a 100644 --- a/libbreezecommon/CMakeLists.txt +++ b/libbreezecommon/CMakeLists.txt @@ -1,64 +1,58 @@ set(BREEZE_COMMON_USE_KDE4 ${USE_KDE4}) if (BREEZE_COMMON_USE_KDE4) ############ Language and toolchain features ############ copied from ECM if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel" AND NOT WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x") endif () endif () ################# dependencies ################# -### FFTW -find_package(FFTW REQUIRED) - ### Qt/KDE if (NOT BREEZE_COMMON_USE_KDE4) find_package(Qt5 REQUIRED CONFIG COMPONENTS Widgets) endif () ################# configuration ################# configure_file(config-breezecommon.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-breezecommon.h ) ################# breezestyle target ################# set(breezecommon_LIB_SRCS - breezeboxshadowhelper.cpp + breezeboxshadowrenderer.cpp ) if (BREEZE_COMMON_USE_KDE4) kde4_add_library(breezecommon4 SHARED ${breezecommon_LIB_SRCS}) generate_export_header(breezecommon4 BASE_NAME breezecommon EXPORT_FILE_NAME breezecommon_export.h) target_link_libraries(breezecommon4 ${KDE4_KDEUI_LIBS}) - target_link_libraries(breezecommon4 ${FFTW_LIBRARIES}) set_target_properties(breezecommon4 PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) install(TARGETS breezecommon4 ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) else () add_library(breezecommon5 ${breezecommon_LIB_SRCS}) generate_export_header(breezecommon5 BASE_NAME breezecommon EXPORT_FILE_NAME breezecommon_export.h) target_link_libraries(breezecommon5 PUBLIC Qt5::Core - Qt5::Gui - PRIVATE - ${FFTW_LIBRARIES}) + Qt5::Gui) set_target_properties(breezecommon5 PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) install(TARGETS breezecommon5 ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) endif () diff --git a/libbreezecommon/breezeboxshadowhelper.cpp b/libbreezecommon/breezeboxshadowhelper.cpp deleted file mode 100644 index 1f345e3a..00000000 --- a/libbreezecommon/breezeboxshadowhelper.cpp +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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 "config-breezecommon.h" - -#include - -#include - -#include - - -namespace Breeze { -namespace BoxShadowHelper { - -namespace { - // FFT approach outperforms naive blur method when blur radius >= 64. - // (was discovered after doing a lot of benchmarks) - const int FFT_BLUR_RADIUS_THRESHOLD = 64; - - // 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 - // Current window size is too small for sigma equal to half of the blur radius. - // As a workaround, sigma blur scale is lowered. With the lowered sigma - // blur scale, area under the kernel equals to 0.98, which is pretty enough. - // Maybe, it should be changed in the future. - const double SIGMA_BLUR_SCALE = 0.4375; -} - -inline int kernelSizeToRadius(int kernelSize) -{ - return (kernelSize - 1) / 2; -} - -inline int radiusToKernelSize(int radius) -{ - return radius * 2 + 1; -} - -QVector computeGaussianKernel(int radius) -{ - QVector kernel; - const int kernelSize = radiusToKernelSize(radius); - kernel.reserve(kernelSize); - - const double sigma = SIGMA_BLUR_SCALE * radius; - const double den = std::sqrt(2.0) * sigma; - double kernelNorm = 0.0; - double lastInt = 0.5 * std::erf((-radius - 0.5) / den); - - for (int i = 0; i < kernelSize; i++) { - const double currInt = 0.5 * std::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; -} - -// Do horizontal pass of the Gaussian filter. Please notice that the result -// is transposed. So, the dst image should have proper size, e.g. if the src -// image have (wxh) size then the dst image should have (hxw) size. The -// result is transposed so we read memory in linear order. -void blurAlphaNaivePass(const QImage &src, QImage &dst, const QVector &kernel) -{ - const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; - const int alphaStride = src.depth() >> 3; - const int radius = kernelSizeToRadius(kernel.size()); - - for (int y = 0; y < src.height(); y++) { - const uchar *in = src.scanLine(y) + alphaOffset; - uchar *out = dst.scanLine(0) + alphaOffset + y * alphaStride; - - for (int x = 0; x < radius; x++) { - const uchar *window = in; - double alpha = 0.0; - for (int k = radius - x; k < kernel.size(); k++) { - alpha += *window * kernel[k]; - window += alphaStride; - } - *out = static_cast(alpha); - out += dst.width() * alphaStride; - } - - for (int x = radius; x < src.width() - radius; x++) { - const uchar *window = in + (x - radius) * alphaStride; - double alpha = 0.0; - for (int k = 0; k < kernel.size(); k++) { - alpha += *window * kernel[k]; - window += alphaStride; - } - *out = static_cast(alpha); - out += dst.width() * alphaStride; - } - - for (int x = src.width() - radius; x < src.width(); x++) { - const uchar *window = in + (x - radius - 1) * alphaStride; - double alpha = 0.0; - const int outside = x + radius - src.width(); - for (int k = 0; k < kernel.size() - outside; k++) { - alpha += *window * kernel[k]; - window += alphaStride; - } - *out = static_cast(alpha); - out += dst.width() * alphaStride; - } - } -} - -// Blur alpha channel of the given image using separable convolution -// gaussian kernel. Not very efficient with big blur radii. -void blurAlphaNaive(QImage &img, int radius) -{ - const QVector kernel = computeGaussianKernel(radius); - QImage tmp(img.height(), img.width(), img.format()); - - blurAlphaNaivePass(img, tmp, kernel); // horizontal pass - blurAlphaNaivePass(tmp, img, kernel); // vertical pass -} - -// 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" -// -void blurAlphaFFT(QImage &img, int radius) -{ - const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; - const int alphaStride = img.depth() >> 3; - const int size = img.width() * img.height(); - - // Use FFTW's malloc function so the returned pointer obeys any - // special alignment restrictions. (e.g. for SIMD acceleration, etc) - // See http://www.fftw.org/fftw3_doc/MekernelSizeToRadius(mory-Allocation.html - fftw_complex *imageIn = fftw_alloc_complex(size); - fftw_complex *imageOut = fftw_alloc_complex(size); - - uchar *data = img.scanLine(0) + alphaOffset; - for (int i = 0; i < size; i++) { - imageIn[i][0] = *data; - imageIn[i][1] = 0.0; - data += alphaStride; - } - - fftw_plan imageFFT = fftw_plan_dft_2d( - img.height(), img.width(), - imageIn, imageOut, - FFTW_FORWARD, FFTW_ESTIMATE); - - fftw_plan imageIFFT = fftw_plan_dft_2d( - img.height(), img.width(), - imageOut, imageIn, - FFTW_BACKWARD, FFTW_ESTIMATE); - - // The computed Gaussian kernel has to have the same size as the input image. - // Please note that the center of the computed Gaussian kernel is placed - // at the top-left corner and the whole kernel is wrapped around so we read - // result in linear order. - // Note: the kernel is computed by taking a product of two 1-D Gaussian kernels. - QVector kernel(size, 0); - const QVector kernel_ = computeGaussianKernel(radius); - for (int y = 0; y < kernel_.size(); y++) { - const int i = (img.height() + y - radius) % img.height(); - for (int x = 0; x < kernel_.size(); x++) { - const int j = (img.width() + x - radius) % img.width(); - kernel[j + i * img.width()] = kernel_[x] * kernel_[y]; - } - } - - fftw_complex *kernelIn = fftw_alloc_complex(kernel.size()); - fftw_complex *kernelOut = fftw_alloc_complex(kernel.size()); - - for (int i = 0; i < size; i++) { - kernelIn[i][0] = kernel[i]; - kernelIn[i][1] = 0.0; - } - - fftw_plan kernelFFT = fftw_plan_dft_2d( - img.height(), img.width(), - kernelIn, kernelOut, - FFTW_FORWARD, FFTW_ESTIMATE); - - // Do actual FFT. - fftw_execute(imageFFT); - fftw_execute(kernelFFT); - - for (int i = 0; i < size; i++) { - const double re = imageOut[i][0] * kernelOut[i][0] - imageOut[i][1] * kernelOut[i][1]; - const double im = imageOut[i][0] * kernelOut[i][1] + imageOut[i][1] * kernelOut[i][0]; - imageOut[i][0] = re; - imageOut[i][1] = im; - } - - fftw_execute(imageIFFT); - - // Copy result back. Please note, result is scaled by `width x height` so we need to scale it down. - const double invSize = 1.0 / size; - data = img.scanLine(0) + alphaOffset; - for (int i = 0; i < size; i++) { - *data = imageIn[i][0] * invSize; - data += alphaStride; - } - - fftw_destroy_plan(kernelFFT); - fftw_destroy_plan(imageFFT); - fftw_destroy_plan(imageIFFT); - - fftw_free(kernelIn); - fftw_free(kernelOut); - - fftw_free(imageIn); - fftw_free(imageOut); -} - -void boxShadow(QPainter *p, const QRect &box, const QPoint &offset, int radius, const QColor &color) -{ - const QSize size = box.size() + 2 * QSize(radius, radius); - -#if BREEZE_COMMON_USE_KDE4 - const qreal dpr = 1.0; -#else - const qreal dpr = p->device()->devicePixelRatioF(); -#endif - - QPainter painter; - - QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); -#if !BREEZE_COMMON_USE_KDE4 - shadow.setDevicePixelRatio(dpr); -#endif - shadow.fill(Qt::transparent); - - painter.begin(&shadow); - painter.fillRect(QRect(QPoint(radius, radius), box.size()), Qt::black); - painter.end(); - - // There is no need to blur RGB channels. Blur the alpha - // channel and then give the shadow a tint of the desired color. - const int radius_ = radius * dpr; - if (radius_ < FFT_BLUR_RADIUS_THRESHOLD) { - blurAlphaNaive(shadow, radius_); - } else { - blurAlphaFFT(shadow, radius_); - } - - painter.begin(&shadow); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(shadow.rect(), color); - painter.end(); - - QRect shadowRect = shadow.rect(); - shadowRect.setSize(shadowRect.size() / dpr); - shadowRect.moveCenter(box.center() + offset); - p->drawImage(shadowRect, shadow); -} - -} // BoxShadowHelper -} // Breeze diff --git a/libbreezecommon/breezeboxshadowhelper.h b/libbreezecommon/breezeboxshadowhelper.h deleted file mode 100644 index 578b16ca..00000000 --- a/libbreezecommon/breezeboxshadowhelper.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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_COMMON_BOXSHADOWHELPER_H -#define BREEZE_COMMON_BOXSHADOWHELPER_H - -#include "breezecommon_export.h" - -#include -#include -#include -#include - - -namespace Breeze { -namespace BoxShadowHelper { - -void BREEZECOMMON_EXPORT boxShadow(QPainter *p, const QRect &box, const QPoint &offset, - int radius, const QColor &color); - -} // BoxShadowHelper -} // Breeze - -#endif // BREEZE_COMMON_BOXSHADOWHELPER_H diff --git a/libbreezecommon/breezeboxshadowrenderer.cpp b/libbreezecommon/breezeboxshadowrenderer.cpp new file mode 100644 index 00000000..b551fd25 --- /dev/null +++ b/libbreezecommon/breezeboxshadowrenderer.cpp @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2018 Vlad Zagorodniy + * + * The box blur implementation is based on AlphaBoxBlur from Firefox. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +// own +#include "breezeboxshadowrenderer.h" + +// auto-generated +#include "config-breezecommon.h" + +// Qt +#include +#include + +namespace Breeze +{ + +static inline int calculateBlurRadius(qreal stdDev) +{ + // See https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement + const qreal gaussianScaleFactor = (3.0 * qSqrt(2.0 * M_PI) / 4.0) * 1.5; + return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5)); +} + +static inline qreal calculateBlurStdDev(int radius) +{ + // https://www.w3.org/TR/css-backgrounds-3/#shadow-blur says that the resulting + // shadow must approximate the image that would be generated by applying to the + // shadow a Gaussian blur with a standard deviation equal to half the blur radius, + // but we had been using a slightly different (non-standard compliant) routine to + // derive the standard deviation before, so in order to not break existing shadow + // params, we're not following the standard. + return radius * 0.43; // TODO: Multiply by 0.5 instead. +} + +static inline QSize calculateBlurExtent(int radius) +{ + const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); + return QSize(blurRadius, blurRadius); +} + +struct BoxLobes +{ + int left; ///< how many pixels sample to the left + int right; ///< how many pixels sample to the right +}; + +/** + * Compute box filter parameters. + * + * @param radius The blur radius. + * @returns Parameters for three box filters. + **/ +static QVector computeLobes(int radius) +{ + const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); + const int z = blurRadius / 3; + + int major; + int minor; + int final; + + switch (blurRadius % 3) { + case 0: + major = z; + minor = z; + final = z; + break; + + case 1: + major = z + 1; + minor = z; + final = z; + break; + + case 2: + major = z + 1; + minor = z; + final = z + 1; + break; + + default: +#if !BREEZE_COMMON_USE_KDE4 + Q_UNREACHABLE(); +#endif + break; + } + + Q_ASSERT(major + minor + final == blurRadius); + + return { + {major, minor}, + {minor, major}, + {final, final} + }; +} + +/** + * Process a row with a box filter. + * + * @param src The start of the row. + * @param dst The destination. + * @param width The width of the row, in pixels. + * @param horizontalStride The number of bytes from one alpha value to the + * next alpha value. + * @param verticalStride The number of bytes from one row to the next row. + * @param lobes Params of the box filter. + * @param transposeInput Whether the input is transposed. + * @param transposeOutput Whether the output should be transposed. + **/ +static inline void boxBlurRowAlpha(const uint8_t *src, uint8_t *dst, int width, int horizontalStride, + int verticalStride, const BoxLobes &lobes, bool transposeInput, + bool transposeOutput) +{ + const int inputStep = transposeInput ? verticalStride : horizontalStride; + const int outputStep = transposeOutput ? verticalStride : horizontalStride; + + const int boxSize = lobes.left + 1 + lobes.right; + const int reciprocal = (1 << 24) / boxSize; + + uint32_t alphaSum = (boxSize + 1) / 2; + + const uint8_t *left = src; + const uint8_t *right = src; + uint8_t *out = dst; + + const uint8_t firstValue = src[0]; + const uint8_t lastValue = src[(width - 1) * inputStep]; + + alphaSum += firstValue * lobes.left; + + const uint8_t *initEnd = src + (boxSize - lobes.left) * inputStep; + while (right < initEnd) { + alphaSum += *right; + right += inputStep; + } + + const uint8_t *leftEnd = src + boxSize * inputStep; + while (right < leftEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += *right - firstValue; + right += inputStep; + out += outputStep; + } + + const uint8_t *centerEnd = src + width * inputStep; + while (right < centerEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += *right - *left; + left += inputStep; + right += inputStep; + out += outputStep; + } + + const uint8_t *rightEnd = dst + width * outputStep; + while (out < rightEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += lastValue - *left; + left += inputStep; + out += outputStep; + } +} + +/** + * Blur the alpha channel of a given image. + * + * @param image The input image. + * @param radius The blur radius. + * @param rect Specifies what part of the image to blur. If nothing is provided, then + * the whole alpha channel of the input image will be blurred. + **/ +static inline void boxBlurAlpha(QImage &image, int radius, const QRect &rect = {}) +{ + if (radius < 2) { + return; + } + + const QVector lobes = computeLobes(radius); + + const QRect blurRect = rect.isNull() ? image.rect() : rect; + + const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; + const int width = blurRect.width(); + const int height = blurRect.height(); + const int rowStride = image.bytesPerLine(); + const int pixelStride = image.depth() >> 3; + + const int bufferStride = qMax(width, height) * pixelStride; + QScopedPointer > buf(new uint8_t[2 * bufferStride]); + uint8_t *buf1 = buf.data(); + uint8_t *buf2 = buf1 + bufferStride; + + // Blur the image in horizontal direction. + for (int i = 0; i < height; ++i) { + uint8_t *row = image.scanLine(blurRect.y() + i) + blurRect.x() * pixelStride + alphaOffset; + boxBlurRowAlpha(row, buf1, width, pixelStride, rowStride, lobes[0], false, false); + boxBlurRowAlpha(buf1, buf2, width, pixelStride, rowStride, lobes[1], false, false); + boxBlurRowAlpha(buf2, row, width, pixelStride, rowStride, lobes[2], false, false); + } + + // Blur the image in vertical direction. + for (int i = 0; i < width; ++i) { + uint8_t *column = image.scanLine(blurRect.y()) + (blurRect.x() + i) * pixelStride + alphaOffset; + boxBlurRowAlpha(column, buf1, height, pixelStride, rowStride, lobes[0], true, false); + boxBlurRowAlpha(buf1, buf2, height, pixelStride, rowStride, lobes[1], false, false); + boxBlurRowAlpha(buf2, column, height, pixelStride, rowStride, lobes[2], false, true); + } +} + +static inline void mirrorTopLeftQuadrant(QImage &image) +{ + const int width = image.width(); + const int height = image.height(); + + const int centerX = qCeil(width * 0.5); + const int centerY = qCeil(height * 0.5); + + const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; + const int stride = image.depth() >> 3; + + for (int y = 0; y < centerY; ++y) { + uint8_t *in = image.scanLine(y) + alphaOffset; + uint8_t *out = in + (width - 1) * stride; + + for (int x = 0; x < centerX; ++x, in += stride, out -= stride) { + *out = *in; + } + } + + for (int y = 0; y < centerY; ++y) { + const uint8_t *in = image.scanLine(y) + alphaOffset; + uint8_t *out = image.scanLine(width - y - 1) + alphaOffset; + + for (int x = 0; x < width; ++x, in += stride, out += stride) { + *out = *in; + } + } +} + +static void renderShadow(QPainter *painter, const QRect &rect, qreal borderRadius, const QPoint &offset, int radius, const QColor &color) +{ + const QSize inflation = calculateBlurExtent(radius); + const QSize size = rect.size() + 2 * inflation; + +#if BREEZE_COMMON_USE_KDE4 + const qreal dpr = 1.0; +#else + const qreal dpr = painter->device()->devicePixelRatioF(); +#endif + + QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); +#if !BREEZE_COMMON_USE_KDE4 + shadow.setDevicePixelRatio(dpr); +#endif + shadow.fill(Qt::transparent); + + QRect boxRect(QPoint(0, 0), rect.size()); + boxRect.moveCenter(QRect(QPoint(0, 0), size).center()); + + const qreal xRadius = 2.0 * borderRadius / boxRect.width(); + const qreal yRadius = 2.0 * borderRadius / boxRect.height(); + + QPainter shadowPainter; + shadowPainter.begin(&shadow); + shadowPainter.setRenderHint(QPainter::Antialiasing); + shadowPainter.setPen(Qt::NoPen); + shadowPainter.setBrush(Qt::black); + shadowPainter.drawRoundedRect(boxRect, xRadius, yRadius); + shadowPainter.end(); + + // Because the shadow texture is symmetrical, that's enough to blur + // only the top-left quadrant and then mirror it. + const QRect blurRect(0, 0, qCeil(shadow.width() * 0.5), qCeil(shadow.height() * 0.5)); + const int scaledRadius = qRound(radius * dpr); + boxBlurAlpha(shadow, scaledRadius, blurRect); + mirrorTopLeftQuadrant(shadow); + + // Give the shadow a tint of the desired color. + shadowPainter.begin(&shadow); + shadowPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + shadowPainter.fillRect(shadow.rect(), color); + shadowPainter.end(); + + // Actually, present the shadow. + QRect shadowRect = shadow.rect(); + shadowRect.setSize(shadowRect.size() / dpr); + shadowRect.moveCenter(rect.center() + offset); + painter->drawImage(shadowRect, shadow); +} + +void BoxShadowRenderer::setBoxSize(const QSize &size) +{ + m_boxSize = size; +} + +void BoxShadowRenderer::setBorderRadius(qreal radius) +{ + m_borderRadius = radius; +} + +void BoxShadowRenderer::setDevicePixelRatio(qreal dpr) +{ + m_dpr = dpr; +} + +void BoxShadowRenderer::addShadow(const QPoint &offset, int radius, const QColor &color) +{ + Shadow shadow = {}; + shadow.offset = offset; + shadow.radius = radius; + shadow.color = color; + m_shadows.append(shadow); +} + +QImage BoxShadowRenderer::render() const +{ + if (m_shadows.isEmpty()) { + return {}; + } + + QSize canvasSize; +#if BREEZE_COMMON_USE_KDE4 + foreach (const Shadow &shadow, m_shadows) { +#else + for (const Shadow &shadow : qAsConst(m_shadows)) { +#endif + canvasSize = canvasSize.expandedTo( + calculateMinimumShadowTextureSize(m_boxSize, shadow.radius, shadow.offset)); + } + + QImage canvas(canvasSize * m_dpr, QImage::Format_ARGB32_Premultiplied); +#if !BREEZE_COMMON_USE_KDE4 + canvas.setDevicePixelRatio(m_dpr); +#endif + canvas.fill(Qt::transparent); + + QRect boxRect(QPoint(0, 0), m_boxSize); + boxRect.moveCenter(QRect(QPoint(0, 0), canvasSize).center()); + + QPainter painter(&canvas); +#if BREEZE_COMMON_USE_KDE4 + foreach (const Shadow &shadow, m_shadows) { +#else + for (const Shadow &shadow : qAsConst(m_shadows)) { +#endif + renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color); + } + painter.end(); + + return canvas; +} + +QSize BoxShadowRenderer::calculateMinimumBoxSize(int radius) +{ + const QSize blurExtent = calculateBlurExtent(radius); + return 2 * blurExtent + QSize(1, 1); +} + +QSize BoxShadowRenderer::calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset) +{ + return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y())); +} + +} // namespace Breeze diff --git a/libbreezecommon/breezeboxshadowrenderer.h b/libbreezecommon/breezeboxshadowrenderer.h new file mode 100644 index 00000000..4c271bee --- /dev/null +++ b/libbreezecommon/breezeboxshadowrenderer.h @@ -0,0 +1,104 @@ +/* + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +// own +#include "breezecommon_export.h" + +// Qt +#include +#include +#include + +namespace Breeze +{ + +class BREEZECOMMON_EXPORT BoxShadowRenderer +{ +public: + // Compiler generated constructors & destructor are fine. + + /** + * Set the size of the box. + * @param size The size of the box. + **/ + void setBoxSize(const QSize &size); + + /** + * Set the radius of box' corners. + * @param radius The border radius, in pixels. + **/ + void setBorderRadius(qreal radius); + + /** + * Set the device pixel ratio of the resulting shadow texture. + * @param dpr The device pixel ratio. + **/ + void setDevicePixelRatio(qreal dpr); + + /** + * Add a shadow. + * @param offset The offset of the shadow. + * @param radius The blur radius. + * @param color The color of the shadow. + **/ + void addShadow(const QPoint &offset, int radius, const QColor &color); + + /** + * Render the shadow. + **/ + QImage render() const; + + /** + * Calculate the minimum size of the box. + * + * This helper computes the minimum size of the box so the shadow behind it has + * full its strength. + * + * @param radius The blur radius of the shadow. + **/ + static QSize calculateMinimumBoxSize(int radius); + + /** + * Calculate the minimum size of the shadow texture. + * + * This helper computes the minimum size of the resulting texture so the shadow + * is not clipped. + * + * @param boxSize The size of the box. + * @param radius The blur radius. + * @param offset The offset of the shadow. + **/ + static QSize calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset); + +private: + QSize m_boxSize; + qreal m_borderRadius = 0.0; + qreal m_dpr = 1.0; + + struct Shadow { + QPoint offset; + int radius; + QColor color; + }; + + QVector m_shadows; +}; + +} // namespace Breeze