diff --git a/CMakeLists.txt b/CMakeLists.txt index b28b820..dfbeb78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,6 @@ add_subdirectory(plugins) install(FILES config/kglobalshortcutsrc DESTINATION "${CUTEFISH_KWIN_CONFIG_DIR}") install(FILES config/kwinrc DESTINATION "${CUTEFISH_KWIN_CONFIG_DIR}") -install(FILES config/kwinrulesrc DESTINATION "${CUTEFISH_KWIN_CONFIG_DIR}") install(DIRECTORY scripts/cutefishlauncher DESTINATION "${CUTEFISH_KWIN_DATA_DIR}/scripts") install(DIRECTORY scripts/cutefish_squash DESTINATION "${CUTEFISH_KWIN_DATA_DIR}/effects") diff --git a/config/kwinrulesrc b/config/kwinrulesrc deleted file mode 100644 index 4aa1422..0000000 --- a/config/kwinrulesrc +++ /dev/null @@ -1,15 +0,0 @@ -[1] -Description=cutefish-dock -desktop=-1 -desktoprule=2 -wmclass=cutefish-dock cutefish-dock -wmclasscomplete=true -wmclassmatch=1 - -[2] -Description=cutefish-launcher -strictgeometry=false -strictgeometryrule=2 -wmclass=cutefish-launcher cutefish-launcher -wmclasscomplete=true -wmclassmatch=1 \ No newline at end of file diff --git a/plugins/decoration/button.cpp b/plugins/decoration/button.cpp index cd1cc0f..3e362b8 100644 --- a/plugins/decoration/button.cpp +++ b/plugins/decoration/button.cpp @@ -54,9 +54,8 @@ void Button::paint(QPainter *painter, const QRectF &repaintArea) } const QRectF buttonRect = geometry(); - const qreal scale = decoration->devicePixelRatio(); - const QRectF hoverRect = buttonRect.adjusted(2 * scale, 2 * scale, -2 * scale, -2 * scale); - const QRectF imageRect = QRectF(0, 0, 24 * scale, 24 * scale); + const QRectF hoverRect = buttonRect.adjusted(2, 2, -2, -2); + const QRectF imageRect = QRectF(0, 0, 24, 24); painter->save(); painter->setRenderHint(QPainter::Antialiasing); diff --git a/plugins/decoration/decoration.cpp b/plugins/decoration/decoration.cpp index 00cb8f9..1b276ed 100644 --- a/plugins/decoration/decoration.cpp +++ b/plugins/decoration/decoration.cpp @@ -70,6 +70,10 @@ bool Decoration::init() updateButtonsGeometry(); }); connect(window, &KDecoration3::DecoratedWindow::heightChanged, this, [this] { updateGeometry(); }); + connect(window, &KDecoration3::DecoratedWindow::nextScaleChanged, this, [this] { + updateButtonPixmaps(); + update(); + }); if (settings()) { connect(settings().get(), &KDecoration3::DecorationSettings::fontChanged, this, [this] { @@ -92,7 +96,7 @@ bool Decoration::init() }); } - // Follow the CutefishOS theme settings (dark mode, scaling) while the window is open. + // Follow the CutefishOS theme settings while the window is open. if (!m_themeSettingsFile.isEmpty()) { m_themeWatcher.addPath(m_themeSettingsFile); connect(&m_themeWatcher, &QFileSystemWatcher::fileChanged, this, [this] { @@ -120,19 +124,9 @@ void Decoration::paint(QPainter *painter, const QRectF &repaintArea) } if (!window()->isShaded()) { - painter->save(); - painter->setRenderHint(QPainter::Antialiasing); - painter->setPen(Qt::NoPen); - painter->setBrush(titleBarBackgroundColor()); - - const bool rounded = !window()->isMaximized() - && (!settings() || settings()->isAlphaChannelSupported()); - if (rounded) { - painter->drawRoundedRect(rect(), m_frameRadius, m_frameRadius); - } else { - painter->drawRect(rect()); - } - painter->restore(); + // The rounded-window effect owns the final frame shape. Paint an opaque, + // square decoration here so the same edge is not anti-aliased twice. + painter->fillRect(rect(), titleBarBackgroundColor()); if (m_leftButtons) { m_leftButtons->paint(painter, repaintArea); @@ -215,16 +209,13 @@ void Decoration::updateShadow() 2 * shadowOverlap, shadowOffset + 2 * shadowOverlap); - // Contrast pixel around the window. - painter.setPen(gradientStopColor(shadowColor, shadowStrength * 0.5)); - painter.setBrush(Qt::NoBrush); - painter.drawRoundedRect(innerRect, -0.5 + m_frameRadius, -0.5 + m_frameRadius); - - // Mask out the area covered by the window itself. + // Mask out the area covered by the window itself. Keep the cutout on the + // integer pixel grid; the old +/-0.5 radii produced a one-pixel contrast + // fringe around the whole window once KWin applied its own rounded mask. painter.setPen(Qt::NoPen); painter.setBrush(Qt::black); painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); - painter.drawRoundedRect(innerRect, 0.5 + m_frameRadius, 0.5 + m_frameRadius); + painter.drawRoundedRect(innerRect, m_frameRadius, m_frameRadius); painter.end(); s_shadow = std::make_shared(); @@ -243,12 +234,7 @@ void Decoration::reloadTheme() { m_themeSettings.sync(); - m_devicePixelRatio = m_themeSettings.value(QStringLiteral("PixelRatio"), 1.0).toReal(); - if (m_devicePixelRatio <= 0) { - m_devicePixelRatio = 1.0; - } m_darkMode = m_themeSettings.value(QStringLiteral("DarkMode"), false).toBool(); - m_frameRadius = qRound(11 * m_devicePixelRatio); updateButtonPixmaps(); updateShadow(); @@ -269,11 +255,24 @@ QPixmap Decoration::loadPixmap(const QString &path) const return {}; } - // The icons are SVGs, render them at the size the buttons are painted with. - reader.setScaledSize(QSize(24, 24) * m_devicePixelRatio); + // Decoration geometry is expressed in logical coordinates on Wayland. + // Rasterize SVGs at the output scale for sharp icons, but keep their + // logical paint size at 24x24. + const qreal scale = outputScale(); + reader.setScaledSize(QSize(qRound(24 * scale), qRound(24 * scale))); return QPixmap::fromImage(reader.read()); } +qreal Decoration::outputScale() const +{ + if (!window()) { + return 1.0; + } + + const qreal scale = window()->nextScale(); + return scale > 0 ? scale : 1.0; +} + QPixmap Decoration::buttonPixmap(KDecoration3::DecorationButtonType type, bool checked) const { switch (type) { @@ -310,7 +309,7 @@ QString Decoration::pixmapPath(KDecoration3::DecorationButtonType type, bool che int Decoration::titleBarHeight() const { - return qMax(1, qRound(m_titleBarHeight * m_devicePixelRatio)); + return m_titleBarHeight; } QColor Decoration::titleBarBackgroundColor() const diff --git a/plugins/decoration/decoration.h b/plugins/decoration/decoration.h index a1ec161..4f4ae4c 100644 --- a/plugins/decoration/decoration.h +++ b/plugins/decoration/decoration.h @@ -22,7 +22,7 @@ public: void paint(QPainter *painter, const QRectF &repaintArea) override; bool darkMode() const { return m_darkMode; } - qreal devicePixelRatio() const { return m_devicePixelRatio; } + qreal outputScale() const; QPixmap buttonPixmap(KDecoration3::DecorationButtonType type, bool checked = false) const; private: @@ -42,7 +42,6 @@ private: KDecoration3::DecorationButtonGroup *m_leftButtons = nullptr; KDecoration3::DecorationButtonGroup *m_rightButtons = nullptr; - qreal m_devicePixelRatio = 1.0; bool m_darkMode = false; int m_titleBarHeight = 30; int m_frameRadius = 11; diff --git a/plugins/roundedwindow/metadata.json b/plugins/roundedwindow/metadata.json index 3691560..607d8c1 100644 --- a/plugins/roundedwindow/metadata.json +++ b/plugins/roundedwindow/metadata.json @@ -14,6 +14,5 @@ "License": "GPL", "Name": "CutefishOS Rounded Corners", "Version": "1" - }, - "X-KDE-Ordering": 5 + } } diff --git a/plugins/roundedwindow/roundedwindow.cpp b/plugins/roundedwindow/roundedwindow.cpp index a6ff81f..121799d 100644 --- a/plugins/roundedwindow/roundedwindow.cpp +++ b/plugins/roundedwindow/roundedwindow.cpp @@ -1,12 +1,14 @@ #include "roundedwindow.h" +#include +#include +#include #include #include #include #include #include -#include #include #include @@ -64,7 +66,7 @@ static QByteArray fragmentShaderSource() "uniform vec4 modulation;\n" "uniform float saturation;\n" "\n" - // Size of the offscreen texture, in logical pixels. + // Size of the offscreen texture, in device pixels. "uniform vec2 textureSize;\n" // Frame of the window inside the texture (x, y, width, height). "uniform vec4 frameRect;\n" @@ -77,34 +79,126 @@ static QByteArray fragmentShaderSource() } source += "\n" - // Signed distance to a rounded rectangle centred on the origin. - "float roundedBoxDistance(vec2 point, vec2 halfSize, float r)\n" + // Convert a point expressed relative to the window frame back to + // the offscreen texture coordinate system. KWin's normalized + // GLTexture matrix flips Y. + "vec2 framePixelToTex(vec2 framePixel)\n" "{\n" - " vec2 q = abs(point) - halfSize + vec2(r);\n" - " return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r;\n" + " vec2 texturePixel = frameRect.xy + framePixel;\n" + " return vec2(texturePixel.x / textureSize.x,\n" + " 1.0 - texturePixel.y / textureSize.y);\n" + "}\n" + "\n"; + + source += "vec4 sampleFramePixel(vec2 framePixel)\n" + "{\n" + " return " + textureLookup + "(sampler, framePixelToTex(framePixel));\n" + "}\n" + "\n" + // The decoration shadow is already part of KWin's offscreen + // texture. Once the opaque frame has been rendered, however, the + // shadow underneath the frame is no longer recoverable by merely + // lowering the frame alpha. Reconstruct each clipped corner from + // the two adjacent native-shadow edges. + "vec4 nativeCornerShadow(vec2 point, bool right, bool bottom)\n" + "{\n" + " const float margin = 3.0;\n" + " vec2 size = frameRect.zw;\n" + " float leftMargin = frameRect.x;\n" + " float topMargin = frameRect.y;\n" + " float rightMargin = textureSize.x - frameRect.x - size.x;\n" + " float bottomMargin = textureSize.y - frameRect.y - size.y;\n" + " bool hasVerticalShadow = right ? rightMargin >= margin : leftMargin >= margin;\n" + " bool hasHorizontalShadow = bottom ? bottomMargin >= margin : topMargin >= margin;\n" + " if (!hasVerticalShadow || !hasHorizontalShadow) {\n" + // CSD windows can have no expanded shadow texture. Transparent + // black keeps the output valid for premultiplied-alpha blending. + " return vec4(0.0);\n" + " }\n" + "\n" + " vec2 a;\n" + " vec2 b;\n" + " if (!right && !bottom) {\n" + " a = vec2(-margin, point.y + point.x + margin);\n" + " b = vec2(point.x + point.y + margin, -margin);\n" + " } else if (right && !bottom) {\n" + " a = vec2(size.x + margin, point.y + (size.x - point.x) + margin);\n" + " b = vec2(point.x - point.y - margin, -margin);\n" + " } else if (!right && bottom) {\n" + " a = vec2(-margin, point.y - point.x - margin);\n" + " b = vec2(point.x + (size.y - point.y) + margin, size.y + margin);\n" + " } else {\n" + " a = vec2(size.x + margin, point.y - (size.x - point.x) - margin);\n" + " b = vec2(point.x - (size.y - point.y) - margin, size.y + margin);\n" + " }\n" + "\n" + " vec4 aColor = sampleFramePixel(a);\n" + " vec4 bColor = sampleFramePixel(b);\n" + " float segment = max(distance(a, b), 0.001);\n" + " return mix(aColor, bColor, clamp(distance(a, point) / segment, 0.0, 1.0));\n" + "}\n" + "\n" + "float cornerCoverage(vec2 point, vec2 center, float r)\n" + "{\n" + // Pixel centres are half a pixel away from the mathematical edge. + // This half-pixel coverage convention matches KWin rounded-corner + // effects and avoids a dark or bright one-pixel halo. + " return clamp(r - distance(point, center) + 0.5, 0.0, 1.0);\n" "}\n" "\n" "void main(void)\n" "{\n" " vec4 texel = " + textureLookup + "(sampler, texcoord0);\n" - " texel *= modulation;\n" - " texel.rgb = mix(vec3(dot(texel.rgb, vec3(0.2126, 0.7152, 0.0722))), texel.rgb, saturation);\n" + " vec4 result = texel;\n" + "\n" + // KWin's GLTexture normalized-coordinate matrix flips the Y axis. + " vec2 texturePoint = vec2(texcoord0.x, 1.0 - texcoord0.y) * textureSize;\n" + " vec2 local = texturePoint - frameRect.xy;\n" + " vec2 size = frameRect.zw;\n" + " float r = min(radius, 0.5 * min(size.x, size.y));\n" "\n" - " vec2 point = texcoord0 * textureSize;\n" - " vec2 halfSize = frameRect.zw * 0.5;\n" - " vec2 centered = point - (frameRect.xy + halfSize);\n" + " if (r > 0.0 && local.x >= 0.0 && local.y >= 0.0\n" + " && local.x <= size.x && local.y <= size.y) {\n" + // Only touch the four radius-by-radius corner squares. Straight + // edges must remain byte-for-byte identical to the captured + // texture; applying a rounded-rectangle SDF to the whole frame can + // introduce a translucent fringe along those edges. + " bool corner = false;\n" + " bool right = false;\n" + " bool bottom = false;\n" + " vec2 center = vec2(r, r);\n" "\n" - // Everything outside of the window frame - the decoration shadow - - // has to be left alone. - " vec2 outside = abs(centered) - halfSize;\n" - " if (max(outside.x, outside.y) > 0.0) {\n" - " " + output + " = texel;\n" - " return;\n" + " if (local.x < r && local.y < r) {\n" + " corner = true;\n" + " } else if (local.x > size.x - r && local.y < r) {\n" + " corner = true;\n" + " right = true;\n" + " center = vec2(size.x - r, r);\n" + " } else if (local.x < r && local.y > size.y - r) {\n" + " corner = true;\n" + " bottom = true;\n" + " center = vec2(r, size.y - r);\n" + " } else if (local.x > size.x - r && local.y > size.y - r) {\n" + " corner = true;\n" + " right = true;\n" + " bottom = true;\n" + " center = vec2(size.x - r, size.y - r);\n" + " }\n" + "\n" + " if (corner) {\n" + " float coverage = cornerCoverage(local, center, r);\n" + " if (coverage < 1.0) {\n" + " vec4 shadow = nativeCornerShadow(local, right, bottom);\n" + " result = mix(shadow, texel, coverage);\n" + " }\n" + " }\n" " }\n" "\n" - " float dist = roundedBoxDistance(centered, halfSize, radius);\n" - " float aa = max(length(fwidth(point)) * 0.5, 0.5);\n" - " " + output + " = texel * (1.0 - smoothstep(-aa, aa, dist));\n" + // Apply KWin's paint modulation after the corner reconstruction so + // both the frame and sampled native shadow fade together. + " result *= modulation;\n" + " result.rgb = mix(vec3(dot(result.rgb, vec3(0.2126, 0.7152, 0.0722))), result.rgb, saturation);\n" + " " + output + " = result;\n" "}\n"; return source; @@ -113,15 +207,54 @@ static QByteArray fragmentShaderSource() RoundedWindow::RoundedWindow() : KWin::OffscreenEffect() { - reconfigure(ReconfigureAll); - - connect(KWin::effects, &KWin::EffectsHandler::windowAdded, this, &RoundedWindow::handleWindowAdded); - connect(KWin::effects, &KWin::EffectsHandler::windowDeleted, this, &RoundedWindow::handleWindowDeleted); - - const auto windows = KWin::effects->stackingOrder(); - for (KWin::EffectWindow *window : windows) { - handleWindowAdded(window); + auto watchWindow = [this](KWin::EffectWindow *window) { + connect(window, &KWin::EffectWindow::windowMaximizedStateAboutToChange, this, + [this](KWin::EffectWindow *w, bool horizontal, bool vertical) { + // Drop the current offscreen texture before either direction + // of the maximize transition. In particular, a restore must + // not reuse the full-screen texture captured while maximized. + unredirect(w); + + if (horizontal && vertical) { + m_maximizingWindows.insert(w); + m_restoringWindows.remove(w); + } else if (isMaximized(w)) { + m_restoringWindows.insert(w); + m_maximizingWindows.remove(w); + } else { + m_maximizingWindows.remove(w); + m_restoringWindows.remove(w); + } + + w->addRepaintFull(); + }); + }; + + connect(KWin::effects, &KWin::EffectsHandler::windowAdded, this, watchWindow); + for (KWin::EffectWindow *window : KWin::effects->stackingOrder()) { + watchWindow(window); } + + connect(KWin::effects, &KWin::EffectsHandler::windowClosed, this, + [this](KWin::EffectWindow *window) { + // A closed EffectWindow becomes "deleted" before the close + // animation has finished. Remember that it was rounded so the + // scale/fade animation keeps drawing the already-rounded FBO + // instead of switching to a square window for its last frames. + if (m_roundedWindows.contains(window)) { + m_closingWindows.insert(window); + } + m_maximizingWindows.remove(window); + m_restoringWindows.remove(window); + }); + + connect(KWin::effects, &KWin::EffectsHandler::windowDeleted, this, + [this](KWin::EffectWindow *window) { + m_closingWindows.remove(window); + m_maximizingWindows.remove(window); + m_restoringWindows.remove(window); + m_roundedWindows.remove(window); + }); } RoundedWindow::~RoundedWindow() = default; @@ -136,87 +269,22 @@ bool RoundedWindow::enabledByDefault() return supported(); } -void RoundedWindow::reconfigure(ReconfigureFlags flags) -{ - Q_UNUSED(flags) - - QSettings settings(QSettings::UserScope, QStringLiteral("cutefishos"), QStringLiteral("theme")); - qreal devicePixelRatio = settings.value(QStringLiteral("PixelRatio"), 1.0).toReal(); - if (devicePixelRatio <= 0) { - devicePixelRatio = 1.0; - } - m_frameRadius = 11 * devicePixelRatio; - - const auto windows = m_redirected; - for (KWin::EffectWindow *window : windows) { - window->addRepaintFull(); - } -} - -void RoundedWindow::handleWindowAdded(KWin::EffectWindow *window) -{ - if (!window) { - return; - } - - connect(window, &KWin::EffectWindow::windowMaximizedStateChanged, this, - [this, window] { updateWindow(window); }); - connect(window, &KWin::EffectWindow::windowFullScreenChanged, this, - [this, window] { updateWindow(window); }); - connect(window, &KWin::EffectWindow::windowFrameGeometryChanged, this, - [this, window] { updateWindow(window); }); - connect(window, &KWin::EffectWindow::windowDecorationChanged, this, - [this, window] { updateWindow(window); }); - - updateWindow(window); -} - -void RoundedWindow::handleWindowDeleted(KWin::EffectWindow *window) -{ - m_redirected.remove(window); -} - -void RoundedWindow::updateWindow(KWin::EffectWindow *window) -{ - const bool wanted = shouldRound(window); - const bool redirected = m_redirected.contains(window); - - if (wanted == redirected) { - return; - } - - if (wanted) { - if (KWin::GLShader *s = shader()) { - redirect(window); - setShader(window, s); - m_redirected.insert(window); - } - } else { - unredirect(window); - m_redirected.remove(window); - } - - window->addRepaintFull(); -} - bool RoundedWindow::shouldRound(KWin::EffectWindow *window) const { - if (!window || window->isDeleted() || !window->isOnCurrentDesktop()) { + if (!window || !window->isManaged()) { return false; } - if (s_allowList.contains(window->windowClass())) { - return true; - } - - if (!window->isManaged() || window->isFullScreen() || isMaximized(window)) { - return false; + // Deleted windows can remain visible while KWin runs the close animation. + // Only keep rounding if this exact window was already rounded before close. + if (window->isDeleted()) { + return m_closingWindows.contains(window); } - // Client side decorated windows - the CutefishOS applications themselves - - // paint their own rounded corners and shadow. Cutting into them would only - // produce artefacts, so this effect is for windows KWin decorates. - if (!window->hasDecoration()) { + // A fully maximized window is square, but as soon as a restore starts we + // need the rounded path available from the first transformed frame. + if (window->isFullScreen() + || (isMaximized(window) && !m_restoringWindows.contains(window))) { return false; } @@ -227,13 +295,31 @@ bool RoundedWindow::shouldRound(KWin::EffectWindow *window) const return false; } + const bool allowListed = s_allowList.contains(window->windowClass()); + if (allowListed) { + return true; + } + + if (!window->hasDecoration()) { + return false; + } + return window->isNormalWindow() || window->isDialog() || window->isUtility(); } bool RoundedWindow::isMaximized(KWin::EffectWindow *window) const { + const QRectF frame = window->frameGeometry(); const QRectF maximizedArea = KWin::effects->clientArea(KWin::MaximizeArea, window); - return window->frameGeometry() == maximizedArea; + + // Wayland geometry can differ from the maximize area by a fractional pixel + // while output scaling and decoration geometry settle. Exact QRectF equality + // makes the effect oscillate on/off at the end of maximize/restore. + constexpr qreal tolerance = 1.0; + return qAbs(frame.x() - maximizedArea.x()) <= tolerance + && qAbs(frame.y() - maximizedArea.y()) <= tolerance + && qAbs(frame.width() - maximizedArea.width()) <= tolerance + && qAbs(frame.height() - maximizedArea.height()) <= tolerance; } KWin::GLShader *RoundedWindow::shader() @@ -255,11 +341,15 @@ void RoundedWindow::prePaintWindow(KWin::EffectWindow *window, KWin::WindowPrePaintData &data, std::chrono::milliseconds presentTime) { - if (m_redirected.contains(window)) { + const bool round = shouldRound(window); + if (round) { // The corners are cut out of the window, so it can no longer be treated - // as an opaque window - otherwise nothing would be blended with what is - // behind it and the cut out corners would stay black. + // as opaque. This also applies while open/close/restore animations + // transform the rounded offscreen texture. data.setTranslucent(); + if (!window->isDeleted()) { + m_roundedWindows.insert(window); + } } KWin::OffscreenEffect::prePaintWindow(window, data, presentTime); @@ -272,23 +362,79 @@ void RoundedWindow::drawWindow(const KWin::RenderTarget &renderTarget, const QRegion ®ion, KWin::WindowPaintData &data) { - if (m_redirected.contains(window)) { - if (KWin::GLShader *s = shader()) { - const QRectF expanded = window->expandedGeometry(); - const QRectF frame = window->frameGeometry(); - - // The offscreen texture covers the expanded geometry of the window, - // texcoord0 runs from (0, 0) to (1, 1) over it. - KWin::ShaderBinder binder(s); - s->setUniform("textureSize", QVector2D(expanded.width(), expanded.height())); - s->setUniform("frameRect", QVector4D(frame.x() - expanded.x(), - frame.y() - expanded.y(), - frame.width(), - frame.height())); - s->setUniform("radius", float(m_frameRadius)); + const bool transformed = mask & KWin::Effect::PAINT_WINDOW_TRANSFORMED; + + // Transformation itself is not a reason to drop rounded corners: opening, + // closing, minimizing and restoring all legitimately transform the window. + // The problematic case is specifically the transition *into* maximized + // state, where KWin cross-fades the previous buffer. Keep that direction on + // the normal path so our offscreen texture cannot be captured recursively. + if (transformed && m_maximizingWindows.contains(window)) { + unredirect(window); + KWin::OffscreenEffect::drawWindow(renderTarget, viewport, window, mask, region, data); + return; + } + + // Clear transition bookkeeping only after the target geometry has settled. + // This avoids losing the state if KWin happens to paint one ordinary frame + // between the about-to-change signal and the scripted animation. + if (!transformed) { + if (m_maximizingWindows.contains(window) && isMaximized(window)) { + m_maximizingWindows.remove(window); + } + if (m_restoringWindows.contains(window) && !isMaximized(window)) { + m_restoringWindows.remove(window); } } + // Keep KWin's offscreen state tied to the current paint pass. Persisting our + // own redirect state across maximize/restore lets an old FBO survive a geometry + // transition and is especially fragile when the Maximize effect cross-fades a + // previous buffer. This is the same model used by maintained KWin rounded-corner + // effects: redirect only while the window currently needs the effect. + if (!shouldRound(window)) { + if (!window->isDeleted()) { + m_roundedWindows.remove(window); + } + unredirect(window); + KWin::OffscreenEffect::drawWindow(renderTarget, viewport, window, mask, region, data); + return; + } + + if (!window->isDeleted()) { + m_roundedWindows.insert(window); + } + + KWin::GLShader *s = shader(); + if (!s) { + unredirect(window); + KWin::OffscreenEffect::drawWindow(renderTarget, viewport, window, mask, region, data); + return; + } + + redirect(window); + setShader(window, s); + + // OffscreenEffect allocates its texture in device pixels. Keep every shader + // geometry in that same coordinate space; using logical sizes here causes the + // mask and shadow boundary to drift at fractional scale. + const qreal scale = window->screen() ? window->screen()->scale() : viewport.scale(); + const QRectF expanded = KWin::snapToPixels(window->expandedGeometry(), scale); + const QRectF frame = KWin::snapToPixels(window->frameGeometry(), scale); + + const QSizeF textureSize(expanded.width() * scale, expanded.height() * scale); + const QPointF frameOffset((frame.x() - expanded.x()) * scale, + (frame.y() - expanded.y()) * scale); + const QSizeF frameSize(frame.width() * scale, frame.height() * scale); + + KWin::ShaderBinder binder(s); + s->setUniform("textureSize", QVector2D(textureSize.width(), textureSize.height())); + s->setUniform("frameRect", QVector4D(frameOffset.x(), + frameOffset.y(), + frameSize.width(), + frameSize.height())); + s->setUniform("radius", float(m_frameRadius * scale)); + KWin::OffscreenEffect::drawWindow(renderTarget, viewport, window, mask, region, data); } diff --git a/plugins/roundedwindow/roundedwindow.h b/plugins/roundedwindow/roundedwindow.h index d64c0f6..0250ae0 100644 --- a/plugins/roundedwindow/roundedwindow.h +++ b/plugins/roundedwindow/roundedwindow.h @@ -30,7 +30,7 @@ public: static bool supported(); static bool enabledByDefault(); - void reconfigure(ReconfigureFlags flags) override; + int requestedEffectChainPosition() const override { return 99; } void prePaintWindow(KWin::EffectWindow *window, KWin::WindowPrePaintData &data, @@ -44,14 +44,14 @@ public: KWin::WindowPaintData &data) override; private: - void handleWindowAdded(KWin::EffectWindow *window); - void handleWindowDeleted(KWin::EffectWindow *window); - void updateWindow(KWin::EffectWindow *window); bool shouldRound(KWin::EffectWindow *window) const; bool isMaximized(KWin::EffectWindow *window) const; KWin::GLShader *shader(); std::unique_ptr m_shader; - QSet m_redirected; + QSet m_roundedWindows; + QSet m_closingWindows; + QSet m_maximizingWindows; + QSet m_restoringWindows; qreal m_frameRadius = 11; };