perf(ui): replace animated-sidebar drop shadows with pre-baked 9-patch overlay
QGraphicsDropShadowEffect forces the host widget through an off-screen software Gaussian blur on every repaint. On the floating sidebar (HomeView, blur 25) and the player right sidebar (PlayerView, blur 35), the blur was re-rasterized on every frame of the 350ms slide animation, the main source of UI jank. Bake the blur once into a 9-patch PNG (cached to CacheLocation/shadows via QGraphicsBlurEffect) and render it at runtime as a cheap 9-slice stretch with zero per-frame blur cost: - utils/shadowpixmap: off-screen bake + on-disk cache, dpr-aware. - components/shadowoverlay: mouse-transparent sibling widget, lazy-baked, paints the 9-patch; optional body cutout for semi-transparent targets so the shadow does not bleed through the body. - homeview/playerview: drop the effect; the overlay tracks the sidebar geometry via the existing eventFilter (Move/Resize/Show/Hide), so every setGeometry/move site and the slide animation drive it for free.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#include "shadowoverlay.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
ShadowOverlay::ShadowOverlay(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
}
|
||||
|
||||
void ShadowOverlay::setShadow(int blurRadius, int cornerRadius, const QColor &color)
|
||||
{
|
||||
m_blurRadius = blurRadius;
|
||||
m_cornerRadius = cornerRadius;
|
||||
m_color = color;
|
||||
m_patch = NinePatchShadow{}; // force rebuild on next paint
|
||||
m_builtDpr = 0.0;
|
||||
update();
|
||||
}
|
||||
|
||||
void ShadowOverlay::setBodyCutout(bool on)
|
||||
{
|
||||
if (m_bodyCutout == on)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_bodyCutout = on;
|
||||
update();
|
||||
}
|
||||
|
||||
void ShadowOverlay::ensurePixmap(qreal dpr)
|
||||
{
|
||||
if (!m_patch.isNull() && qFuzzyCompare(m_builtDpr, dpr))
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_patch = ShadowPixmap::make(m_blurRadius, m_cornerRadius, m_color, dpr);
|
||||
m_builtDpr = dpr;
|
||||
}
|
||||
|
||||
void ShadowOverlay::paintEvent(QPaintEvent *)
|
||||
{
|
||||
const qreal dpr = devicePixelRatioF();
|
||||
ensurePixmap(dpr);
|
||||
if (m_patch.isNull())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int b = m_patch.border;
|
||||
const int t = m_patch.total;
|
||||
const qreal s = m_patch.dpr;
|
||||
const QRect r = rect();
|
||||
|
||||
// Degenerate target (smaller than the two fixed borders): nothing sensible
|
||||
// to slice, skip rather than draw garbage.
|
||||
if (r.width() < 2 * b || r.height() < 2 * b)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 3x3 grid: corners fixed, edges/centre stretched. Target columns/rows are
|
||||
// logical; source columns/rows are physical (pixmap carries no dpr).
|
||||
const int xt[4] = {r.left(), r.left() + b, r.right() + 1 - b, r.right() + 1};
|
||||
const int yt[4] = {r.top(), r.top() + b, r.bottom() + 1 - b, r.bottom() + 1};
|
||||
const qreal xs[4] = {0.0, b * s, (t - b) * s, t * s};
|
||||
const qreal ys[4] = {0.0, b * s, (t - b) * s, t * s};
|
||||
|
||||
QPainter p(this);
|
||||
if (m_bodyCutout)
|
||||
{
|
||||
// Paint only the ring outside the target's footprint.
|
||||
const int reachPx = reach();
|
||||
const QRect body = r.adjusted(reachPx, reachPx, -reachPx, -reachPx);
|
||||
if (body.isValid())
|
||||
{
|
||||
p.setClipRegion(QRegion(r).subtracted(QRegion(body)));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
for (int j = 0; j < 3; ++j)
|
||||
{
|
||||
const QRectF target(xt[i], yt[j], xt[i + 1] - xt[i], yt[j + 1] - yt[j]);
|
||||
const QRectF source(xs[i], ys[j], xs[i + 1] - xs[i], ys[j + 1] - ys[j]);
|
||||
p.drawPixmap(target, m_patch.pixmap, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user