[Client] Fix user list banner art rendering under display scaling (#7160)

Cached card art pixmaps carry the screen device pixel ratio, so both
banner painters did their crop math on scaled pixels and blitted the
result at raw over logical size, clipping art into its top left quadrant
on any display above 100 percent

Normalize a local copy to DPR 1 before crop math in UserListPainter and
the popup header, clamp srcX and srcY bounds against stored zoom below 1,
keep shared cache entries untouched

Took 15 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-23 11:44:59 +02:00 committed by GitHub
parent 63a970045a
commit c42fb6691d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 33 additions and 15 deletions

View file

@ -189,22 +189,31 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
// ── Card art background ─────────────────────────────────────────────────── // ── Card art background ───────────────────────────────────────────────────
if (!cardArt.isNull()) { if (!cardArt.isNull()) {
// Same DPR normalization as UserListPainter::drawCardArt: the cache
// carries screen scaled pixmaps on HiDPI displays, the math below is
// in raw pixels.
QPixmap art = cardArt;
art.setDevicePixelRatio(1.0);
const int w = rect.width(); const int w = rect.width();
const int h = rect.height(); const int h = rect.height();
const int mL = qRound(w * params.marginPctL); const int mL = qRound(w * params.marginPctL);
const int mR = qRound(w * params.marginPctR); const int mR = qRound(w * params.marginPctR);
const int dW = w - mL - mR; const int dW = w - mL - mR;
const double base = qMax(double(dW) / cardArt.width(), double(h) / cardArt.height()); const double base = qMax(double(dW) / art.width(), double(h) / art.height());
const double scale = base * params.zoom; const double scale = base * params.zoom;
const int sW = qRound(cardArt.width() * scale); const int sW = qRound(art.width() * scale);
const int sH = qRound(cardArt.height() * scale); const int sH = qRound(art.height() * scale);
const QPixmap scaled = cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); const QPixmap scaled = art.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
const int srcX = (sW - dW) / 2; // Clamp against stored zoom < 1, which can push srcX negative and silently
const int srcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h)); // underfill the strip with transparent padding
const int safeSrcX = qBound(0, (sW - dW) / 2, qMax(0, sW - dW));
const int safeSrcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h));
QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); QImage img =
scaled.copy(safeSrcX, safeSrcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
{ {
QPainter mask(&img); QPainter mask(&img);
mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); mask.setCompositionMode(QPainter::CompositionMode_DestinationIn);
@ -361,7 +370,7 @@ void UserInfoPopup::buildUi()
header = new UserInfoHeaderWidget(this); header = new UserInfoHeaderWidget(this);
root->addWidget(header); root->addWidget(header);
// Action area rebuilt per user // Action area, rebuilt per user
actionArea = new QWidget(this); actionArea = new QWidget(this);
root->addWidget(actionArea); root->addWidget(actionArea);
@ -402,7 +411,7 @@ void UserInfoPopup::buildUi()
root->addWidget(gamesView); root->addWidget(gamesView);
// Close button — positioned absolutely in the top-right corner // Close button, positioned absolutely in the top right corner
closeBtn = new QPushButton(QStringLiteral(""), this); closeBtn = new QPushButton(QStringLiteral(""), this);
closeBtn->setFixedSize(22, 22); closeBtn->setFixedSize(22, 22);
closeBtn->setFlat(true); closeBtn->setFlat(true);
@ -673,7 +682,7 @@ void UserInfoPopup::showForUser(const QString &userName,
gamesStatus->setText(tr("Loading games…")); gamesStatus->setText(tr("Loading games…"));
gamesStatus->show(); gamesStatus->show();
// Close button — top-right corner, above everything // Close button, top right corner, above everything
closeBtn->move(PopupWidth - closeBtn->width() - 6, 6); closeBtn->move(PopupWidth - closeBtn->width() - 6, 6);
closeBtn->raise(); closeBtn->raise();
@ -702,7 +711,7 @@ void UserInfoPopup::fetchGames()
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
{ {
if (forUser != currentUser) { if (forUser != currentUser) {
return; // stale response different user showing now return; // stale response, different user showing now
} }
gamesModel->clear(); gamesModel->clear();
@ -763,4 +772,4 @@ void UserInfoPopup::leaveEvent(QEvent *e)
{ {
QFrame::leaveEvent(e); QFrame::leaveEvent(e);
emit mouseLeftPopup(); emit mouseLeftPopup();
} }

View file

@ -155,6 +155,12 @@ void UserListPainter::drawCardArt(QPainter *painter,
return; return;
} }
// CardPictureLoader::getPixmap tags its output with the screen's
// devicePixelRatio on HiDPI displays. Every calculation below is in raw
// pixels, so normalize to 1.0 or the crop renders at 1/dpr scale anchored
// to the top left corner of the row.
art.setDevicePixelRatio(1.0);
const int cardH = rect.height() - 4; const int cardH = rect.height() - 4;
const int totalW = cardRight - rect.left(); const int totalW = cardRight - rect.left();
const int marginL = qRound(totalW * params.marginPctL); const int marginL = qRound(totalW * params.marginPctL);
@ -172,11 +178,14 @@ void UserListPainter::drawCardArt(QPainter *painter,
const int srcX = (scaledW - drawW) / 2; const int srcX = (scaledW - drawW) / 2;
const int srcY = qRound((scaledH - cardH) * params.verticalOffset); const int srcY = qRound((scaledH - cardH) * params.verticalOffset);
// Clamp srcY so we never copy outside the pixmap bounds // Clamp so we never copy outside the pixmap bounds. srcX can go negative
// for stored zoom values below 1, which would silently underfill the
// strip with transparent padding.
const int safeSrcX = qBound(0, srcX, qMax(0, scaledW - drawW));
const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH)); const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH));
QImage img = QImage img =
scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); scaled.copy(safeSrcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
{ {
QPainter mask(&img); QPainter mask(&img);
@ -403,4 +412,4 @@ void UserListPainter::paint(QPainter *painter,
drawBadges(painter, option, rect, cardRight, badges, online, style); drawBadges(painter, option, rect, cardRight, badges, online, style);
painter->restore(); painter->restore();
} }