From c42fb6691d556890db3a326ce0df0f4c00fa2f13 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:44:59 +0200 Subject: [PATCH] [Client] Fix user list banner art rendering under display scaling (#7160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../widgets/server/user/user_info_popup.cpp | 33 ++++++++++++------- .../widgets/server/user/user_list_painter.cpp | 15 +++++++-- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index 014d3d4c3..f6f34a6a5 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -189,22 +189,31 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) // ── Card art background ─────────────────────────────────────────────────── 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 h = rect.height(); const int mL = qRound(w * params.marginPctL); const int mR = qRound(w * params.marginPctR); 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 int sW = qRound(cardArt.width() * scale); - const int sH = qRound(cardArt.height() * scale); + const int sW = qRound(art.width() * scale); + const int sH = qRound(art.height() * scale); - const QPixmap scaled = cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - const int srcX = (sW - dW) / 2; - const int srcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h)); + const QPixmap scaled = art.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + // Clamp against stored zoom < 1, which can push srcX negative and silently + // 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); mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); @@ -361,7 +370,7 @@ void UserInfoPopup::buildUi() header = new UserInfoHeaderWidget(this); root->addWidget(header); - // Action area — rebuilt per user + // Action area, rebuilt per user actionArea = new QWidget(this); root->addWidget(actionArea); @@ -402,7 +411,7 @@ void UserInfoPopup::buildUi() 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->setFixedSize(22, 22); closeBtn->setFlat(true); @@ -673,7 +682,7 @@ void UserInfoPopup::showForUser(const QString &userName, gamesStatus->setText(tr("Loading games…")); gamesStatus->show(); - // Close button — top-right corner, above everything + // Close button, top right corner, above everything closeBtn->move(PopupWidth - closeBtn->width() - 6, 6); closeBtn->raise(); @@ -702,7 +711,7 @@ void UserInfoPopup::fetchGames() void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) { if (forUser != currentUser) { - return; // stale response — different user showing now + return; // stale response, different user showing now } gamesModel->clear(); @@ -763,4 +772,4 @@ void UserInfoPopup::leaveEvent(QEvent *e) { QFrame::leaveEvent(e); emit mouseLeftPopup(); -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index 5a4723065..82f2887c8 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -155,6 +155,12 @@ void UserListPainter::drawCardArt(QPainter *painter, 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 totalW = cardRight - rect.left(); const int marginL = qRound(totalW * params.marginPctL); @@ -172,11 +178,14 @@ void UserListPainter::drawCardArt(QPainter *painter, const int srcX = (scaledW - drawW) / 2; 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)); 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); @@ -403,4 +412,4 @@ void UserListPainter::paint(QPainter *painter, drawBadges(painter, option, rect, cardRight, badges, online, style); painter->restore(); -} \ No newline at end of file +}