From febcf464fe9f9caa78fbfb0d3fb2db8cb372ee22 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 12 May 2024 13:59:14 +0200 Subject: [PATCH] Use Qt's High-DPI scaling on Windows (#4868) --- CHANGELOG.md | 2 + resources/qss/settings.qss | 6 +- src/RunGui.cpp | 4 - src/main.cpp | 5 - src/messages/MessageElement.cpp | 10 +- src/messages/layouts/MessageLayout.cpp | 13 +- src/messages/layouts/MessageLayout.hpp | 5 +- .../layouts/MessageLayoutContainer.cpp | 8 +- .../layouts/MessageLayoutContainer.hpp | 12 +- src/widgets/AttachedWindow.cpp | 8 +- src/widgets/BaseWidget.cpp | 13 - src/widgets/BaseWidget.hpp | 2 - src/widgets/BaseWindow.cpp | 537 +++++++++++------- src/widgets/BaseWindow.hpp | 9 +- src/widgets/Label.cpp | 19 +- src/widgets/TooltipEntryWidget.cpp | 1 + src/widgets/dialogs/SettingsDialog.cpp | 21 +- src/widgets/helper/Button.cpp | 54 +- src/widgets/helper/ChannelView.cpp | 29 +- src/widgets/helper/NotebookTab.cpp | 19 +- tests/src/MessageLayout.cpp | 2 +- 21 files changed, 459 insertions(+), 320 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 631b3eb1d..ef547e8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ ## Unversioned - Major: Release plugins alpha. (#5288) +- Major: Improve high-DPI support on Windows. (#4868) - Minor: Add option to customise Moderation buttons with images. (#5369) - Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) +- Dev: Use Qt's high DPI scaling. (#4868) - Dev: Add doxygen build target. (#5377) - Dev: Make printing of strings in tests easier. (#5379) - Dev: Refactor and document `Scrollbar`. (#5334) diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss index 6d5114423..93c69b603 100644 --- a/resources/qss/settings.qss +++ b/resources/qss/settings.qss @@ -1,11 +1,11 @@ * { - font-size: px; + font-size: 14px; font-family: "Segoe UI"; } QCheckBox::indicator { - width: px; - height: px; + width: 14px; + height: 14px; } chatterino--ComboBox { diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 13012957d..6fba9c6af 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -86,10 +86,6 @@ namespace { QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); #endif -#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); -#endif - QApplication::setStyle(QStyleFactory::create("Fusion")); #ifndef Q_OS_MAC diff --git a/src/main.cpp b/src/main.cpp index ef59af0c5..8da92a45c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,11 +26,6 @@ using namespace chatterino; int main(int argc, char **argv) { - // TODO: This is a temporary fix (see #4552). -#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); -#endif - QApplication a(argc, argv); QCoreApplication::setApplicationName("chatterino"); diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index b11e82be1..3d5ab64e7 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -155,8 +155,8 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + auto image = this->emote_->images.getImageOrLoaded( + container.getImageScale()); if (image->isEmpty()) { return; @@ -210,7 +210,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto images = this->getLoadedImages(container.getScale()); + auto images = this->getLoadedImages(container.getImageScale()); if (images.empty()) { return; @@ -364,7 +364,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + this->emote_->images.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) { return; @@ -798,7 +798,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { const auto &image = - this->images_.getImageOrLoaded(container.getScale()); + this->images_.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) { return; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 126bb40c9..8a20b05cc 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -74,7 +74,8 @@ int MessageLayout::getWidth() const // Layout // return true if redraw is required -bool MessageLayout::layout(int width, float scale, MessageElementFlags flags, +bool MessageLayout::layout(int width, float scale, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer) { // BenchmarkGuard benchmark("MessageLayout::layout()"); @@ -106,6 +107,8 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags, // check if dpi changed layoutRequired |= this->scale_ != scale; this->scale_ = scale; + layoutRequired |= this->imageScale_ != imageScale; + this->imageScale_ = imageScale; if (!layoutRequired) { @@ -148,7 +151,8 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) bool hideSimilar = getSettings()->hideSimilar; bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage); - this->container_.beginLayout(width, this->scale_, messageFlags); + this->container_.beginLayout(width, this->scale_, this->imageScale_, + messageFlags); for (const auto &element : this->message_->elements) { @@ -288,16 +292,11 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width) } // Create new buffer -#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) this->buffer_ = std::make_unique( int(width * painter.device()->devicePixelRatioF()), int(this->container_.getHeight() * painter.device()->devicePixelRatioF())); this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF()); -#else - this->buffer_ = std::make_unique( - width, std::max(16, this->container_.getHeight())); -#endif this->bufferValid_ = false; DebugCount::increase("message drawing buffers"); diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index f54f57d2a..01958ddf2 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -56,8 +56,8 @@ public: MessageLayoutFlags flags; - bool layout(int width, float scale_, MessageElementFlags flags, - bool shouldInvalidateBuffer); + bool layout(int width, float scale_, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer); // Painting MessagePaintResult paint(const MessagePaintContext &ctx); @@ -128,6 +128,7 @@ private: int currentLayoutWidth_ = -1; int layoutState_ = -1; float scale_ = -1; + float imageScale_ = -1.F; MessageElementFlags currentWordFlags_; #ifdef FOURTF diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 29d70e0a1..15a7d71af 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -30,7 +30,7 @@ constexpr const QMargins MARGIN{8, 4, 8, 4}; namespace chatterino { void MessageLayoutContainer::beginLayout(int width, float scale, - MessageFlags flags) + float imageScale, MessageFlags flags) { this->elements_.clear(); this->lines_.clear(); @@ -45,6 +45,7 @@ void MessageLayoutContainer::beginLayout(int width, float scale, this->width_ = width; this->height_ = 0; this->scale_ = scale; + this->imageScale_ = imageScale; this->flags_ = flags; auto mediumFontMetrics = getIApp()->getFonts()->getFontMetrics(FontStyle::ChatMedium, scale); @@ -526,6 +527,11 @@ float MessageLayoutContainer::getScale() const return this->scale_; } +float MessageLayoutContainer::getImageScale() const +{ + return this->imageScale_; +} + bool MessageLayoutContainer::isCollapsed() const { return this->isCollapsed_; diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index ed3c1a7a6..dde3f4d45 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -32,7 +32,8 @@ struct MessageLayoutContainer { * This will reset all line calculations, and will be considered incomplete * until the accompanying end function has been called */ - void beginLayout(int width_, float scale_, MessageFlags flags_); + void beginLayout(int width, float scale, float imageScale, + MessageFlags flags); /** * Finish the layout process of this message @@ -146,6 +147,11 @@ struct MessageLayoutContainer { */ float getScale() const; + /** + * Returns the image scale + */ + float getImageScale() const; + /** * Returns true if this message is collapsed */ @@ -270,6 +276,10 @@ private: // variables float scale_ = 1.F; + /** + * Scale factor for images + */ + float imageScale_ = 1.F; int width_ = 0; MessageFlags flags_{}; /** diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 5ce232a1f..b83afb65d 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -270,20 +270,22 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) } float scale = 1.f; + float ourScale = 1.F; if (auto dpi = getWindowDpi(attached)) { scale = *dpi / 96.f; + ourScale = scale / this->devicePixelRatio(); for (auto w : this->ui_.split->findChildren()) { - w->setOverrideScale(scale); + w->setOverrideScale(ourScale); } - this->ui_.split->setOverrideScale(scale); + this->ui_.split->setOverrideScale(ourScale); } if (this->height_ != -1) { - this->ui_.split->setFixedWidth(int(this->width_ * scale)); + this->ui_.split->setFixedWidth(int(this->width_ * ourScale)); // offset int o = this->fullscreen_ ? 0 : 8; diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index 5302d0397..5e2c932fd 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -120,19 +120,6 @@ void BaseWidget::setScaleIndependantHeight(int value) QSize(this->scaleIndependantSize_.width(), value)); } -float BaseWidget::qtFontScale() const -{ - if (auto *window = dynamic_cast(this->window())) - { - // ensure no div by 0 - return this->scale() / std::max(0.01f, window->nativeScale_); - } - else - { - return this->scale(); - } -} - void BaseWidget::childEvent(QChildEvent *event) { if (event->added()) diff --git a/src/widgets/BaseWidget.hpp b/src/widgets/BaseWidget.hpp index 2e9c04728..4fdc421cd 100644 --- a/src/widgets/BaseWidget.hpp +++ b/src/widgets/BaseWidget.hpp @@ -34,8 +34,6 @@ public: void setScaleIndependantWidth(int value); void setScaleIndependantHeight(int value); - float qtFontScale() const; - protected: void childEvent(QChildEvent *) override; void showEvent(QShowEvent *) override; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 81ed90b28..5a1df70af 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -29,12 +29,163 @@ # pragma comment(lib, "Dwmapi.lib") # include - -# define WM_DPICHANGED 0x02E0 +# include #endif #include "widgets/helper/TitlebarButton.hpp" +namespace { + +#ifdef USEWINSDK + +// From kHiddenTaskbarSize in Firefox +constexpr UINT HIDDEN_TASKBAR_SIZE = 2; + +bool isWindows11OrGreater() +{ + static const bool result = [] { + // This calls RtlGetVersion under the hood so we don't have to. + // The micro version corresponds to dwBuildNumber. + auto version = QOperatingSystemVersion::current(); + return (version.majorVersion() > 10) || + (version.microVersion() >= 22000); + }(); + + return result; +} + +/// Finds the taskbar HWND on a specific monitor (or any) +HWND findTaskbarWindow(LPRECT rcMon = nullptr) +{ + HWND taskbar = nullptr; + RECT taskbarRect; + // return value of IntersectRect, unused + RECT intersectionRect; + + while ((taskbar = FindWindowEx(nullptr, taskbar, L"Shell_TrayWnd", + nullptr)) != nullptr) + { + if (!rcMon) + { + // no monitor was specified, return the first encountered window + break; + } + if (GetWindowRect(taskbar, &taskbarRect) != 0 && + IntersectRect(&intersectionRect, &taskbarRect, rcMon) != 0) + { + // taskbar intersects with the monitor - this is the one + break; + } + } + + return taskbar; +} + +/// Gets the edge of the taskbar if it's automatically hidden +std::optional hiddenTaskbarEdge(LPRECT rcMon = nullptr) +{ + HWND taskbar = findTaskbarWindow(rcMon); + if (!taskbar) + { + return std::nullopt; + } + + APPBARDATA state = {sizeof(state), taskbar}; + APPBARDATA pos = {sizeof(pos), taskbar}; + + auto appBarState = + static_cast(SHAppBarMessage(ABM_GETSTATE, &state)); + if ((appBarState & ABS_AUTOHIDE) == 0) + { + return std::nullopt; + } + + if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos) == 0) + { + qCDebug(chatterinoApp) << "Failed to get taskbar pos"; + return ABE_BOTTOM; + } + + return pos.uEdge; +} + +/// @brief Gets the window borders for @a hwnd +/// +/// Each side of the returned RECT has the correct sign, so they can be added +/// to a window rect. +/// Shrinking by 1px would return {left: 1, top: 1, right: -1, left: -1}. +RECT windowBordersFor(HWND hwnd, bool isMaximized) +{ + RECT margins{0, 0, 0, 0}; + + auto addBorders = isMaximized || isWindows11OrGreater(); + if (addBorders) + { + auto dpi = GetDpiForWindow(hwnd); + auto systemMetric = [&](auto index) { + if (dpi != 0) + { + return GetSystemMetricsForDpi(index, dpi); + } + return GetSystemMetrics(index); + }; + + auto paddedBorder = systemMetric(SM_CXPADDEDBORDER); + auto borderWidth = systemMetric(SM_CXSIZEFRAME) + paddedBorder; + auto borderHeight = systemMetric(SM_CYSIZEFRAME) + paddedBorder; + + margins.left += borderWidth; + margins.right -= borderWidth; + if (isMaximized) + { + margins.top += borderHeight; + } + margins.bottom -= borderHeight; + } + + if (isMaximized) + { + auto *hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi; + mi.cbSize = sizeof(mi); + auto *monitor = [&]() -> LPRECT { + if (GetMonitorInfo(hMonitor, &mi)) + { + return &mi.rcMonitor; + } + return nullptr; + }(); + + auto edge = hiddenTaskbarEdge(monitor); + if (edge) + { + switch (*edge) + { + case ABE_LEFT: + margins.left += HIDDEN_TASKBAR_SIZE; + break; + case ABE_RIGHT: + margins.right -= HIDDEN_TASKBAR_SIZE; + break; + case ABE_TOP: + margins.top += HIDDEN_TASKBAR_SIZE; + break; + case ABE_BOTTOM: + margins.bottom -= HIDDEN_TASKBAR_SIZE; + break; + default: + break; + } + } + } + + return margins; +} + +#endif + +} // namespace + namespace chatterino { BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) @@ -117,95 +268,80 @@ float BaseWindow::scale() const return std::max(0.01f, this->overrideScale().value_or(this->scale_)); } -float BaseWindow::qtFontScale() const -{ - return this->scale() / std::max(0.01F, this->nativeScale_); -} - void BaseWindow::init() { #ifdef USEWINSDK if (this->hasCustomWindowFrame()) { // CUSTOM WINDOW FRAME - QVBoxLayout *layout = new QVBoxLayout(); + auto *layout = new QVBoxLayout(this); this->ui_.windowLayout = layout; - layout->setContentsMargins(1, 1, 1, 1); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - this->setLayout(layout); + + if (!this->frameless_) { - if (!this->frameless_) - { - QHBoxLayout *buttonLayout = this->ui_.titlebarBox = - new QHBoxLayout(); - buttonLayout->setContentsMargins(0, 0, 0, 0); - layout->addLayout(buttonLayout); + QHBoxLayout *buttonLayout = this->ui_.titlebarBox = + new QHBoxLayout(); + buttonLayout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(buttonLayout); - // title - Label *title = new Label; - QObject::connect(this, &QWidget::windowTitleChanged, - [title](const QString &text) { - title->setText(text); - }); + // title + Label *title = new Label; + QObject::connect(this, &QWidget::windowTitleChanged, + [title](const QString &text) { + title->setText(text); + }); - QSizePolicy policy(QSizePolicy::Ignored, - QSizePolicy::Preferred); - policy.setHorizontalStretch(1); - title->setSizePolicy(policy); - buttonLayout->addWidget(title); - this->ui_.titleLabel = title; + QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); + policy.setHorizontalStretch(1); + title->setSizePolicy(policy); + buttonLayout->addWidget(title); + this->ui_.titleLabel = title; - // buttons - TitleBarButton *_minButton = new TitleBarButton; - _minButton->setButtonStyle(TitleBarButtonStyle::Minimize); - TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); - TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setButtonStyle(TitleBarButtonStyle::Close); + // buttons + auto *minButton = new TitleBarButton; + minButton->setButtonStyle(TitleBarButtonStyle::Minimize); + auto *maxButton = new TitleBarButton; + maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); + auto *exitButton = new TitleBarButton; + exitButton->setButtonStyle(TitleBarButtonStyle::Close); - QObject::connect(_minButton, &TitleBarButton::leftClicked, this, - [this] { - this->setWindowState(Qt::WindowMinimized | - this->windowState()); - }); - QObject::connect(_maxButton, &TitleBarButton::leftClicked, this, - [this, _maxButton] { - this->setWindowState( - _maxButton->getButtonStyle() != + QObject::connect(minButton, &TitleBarButton::leftClicked, this, + [this] { + this->setWindowState(Qt::WindowMinimized | + this->windowState()); + }); + QObject::connect( + maxButton, &TitleBarButton::leftClicked, this, + [this, maxButton] { + this->setWindowState(maxButton->getButtonStyle() != TitleBarButtonStyle::Maximize ? Qt::WindowActive : Qt::WindowMaximized); - }); - QObject::connect(_exitButton, &TitleBarButton::leftClicked, - this, [this] { - this->close(); - }); + }); + QObject::connect(exitButton, &TitleBarButton::leftClicked, this, + [this] { + this->close(); + }); - this->ui_.titlebarButtons = new TitleBarButtons( - this, _minButton, _maxButton, _exitButton); + this->ui_.titlebarButtons = + new TitleBarButtons(this, minButton, maxButton, exitButton); - this->ui_.buttons.push_back(_minButton); - this->ui_.buttons.push_back(_maxButton); - this->ui_.buttons.push_back(_exitButton); + this->ui_.buttons.push_back(minButton); + this->ui_.buttons.push_back(maxButton); + this->ui_.buttons.push_back(exitButton); - // buttonLayout->addStretch(1); - buttonLayout->addWidget(_minButton); - buttonLayout->addWidget(_maxButton); - buttonLayout->addWidget(_exitButton); - buttonLayout->setSpacing(0); - } + buttonLayout->addWidget(minButton); + buttonLayout->addWidget(maxButton); + buttonLayout->addWidget(exitButton); + buttonLayout->setSpacing(0); } + this->ui_.layoutBase = new BaseWidget(this); this->ui_.layoutBase->setContentsMargins(1, 0, 1, 1); layout->addWidget(this->ui_.layoutBase); } - -// DPI -// auto dpi = getWindowDpi(this->safeHWND()); - -// if (dpi) { -// this->scale = dpi.value() / 96.f; -// } #endif // TopMost flag overrides setting @@ -571,29 +707,8 @@ void BaseWindow::resizeEvent(QResizeEvent *) } #ifdef USEWINSDK - if (this->hasCustomWindowFrame() && !this->isResizeFixing_) - { - this->isResizeFixing_ = true; - QTimer::singleShot(50, this, [this] { - auto hwnd = this->safeHWND(); - if (!hwnd) - { - this->isResizeFixing_ = false; - return; - } - RECT rect; - ::GetWindowRect(*hwnd, &rect); - ::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left + 1, - rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); - ::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left, - rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER); - QTimer::singleShot(10, this, [this] { - this->isResizeFixing_ = false; - }); - }); - } - this->calcButtonsSizes(); + this->updateRealSize(); #endif } @@ -655,10 +770,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, switch (msg->message) { - case WM_DPICHANGED: - returnValue = this->handleDPICHANGED(msg); - break; - case WM_SHOWWINDOW: returnValue = this->handleSHOWWINDOW(msg); break; @@ -697,12 +808,15 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, { *result = 0; returnValue = true; - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); - QPoint globalPos(x, y); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + + // TODO(nerix): use TrackMouseEvent here this->ui_.titlebarButtons->hover(msg->wParam, globalPos); this->lastEventWasNcMouseMove_ = true; } @@ -748,12 +862,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, *result = 0; auto ht = msg->wParam; - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); - QPoint globalPos(x, y); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + if (msg->message == WM_NCLBUTTONDOWN) { this->ui_.titlebarButtons->mousePress(ht, globalPos); @@ -784,7 +900,7 @@ void BaseWindow::scaleChangedEvent(float scale) #endif this->setFont( - getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); + getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->scale())); } void BaseWindow::paintEvent(QPaintEvent *) @@ -802,10 +918,9 @@ void BaseWindow::paintEvent(QPaintEvent *) void BaseWindow::updateScale() { - auto scale = - this->nativeScale_ * (this->flags_.has(DisableCustomScaling) - ? 1 - : getSettings()->getClampedUiScale()); + auto scale = this->flags_.has(DisableCustomScaling) + ? 1 + : getSettings()->getClampedUiScale(); this->setScale(scale); @@ -815,6 +930,22 @@ void BaseWindow::updateScale() } } +#ifdef USEWINSDK +void BaseWindow::updateRealSize() +{ + auto hwnd = this->safeHWND(); + if (!hwnd) + { + return; + } + + RECT real; + ::GetWindowRect(*hwnd, &real); + this->realBounds_ = QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); +} +#endif + void BaseWindow::calcButtonsSizes() { if (!this->shown_) @@ -846,34 +977,28 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter) { QColor bg = this->overrideBackgroundColor_.value_or( this->theme->window.background); - painter.fillRect(QRect(1, 2, this->width() - 2, this->height() - 3), - bg); + if (this->isMaximized_) + { + painter.fillRect(this->rect(), bg); + } + else + { + // Draw a border that's exactly 1px wide + // + // There is a bug where the border can get px wide while dragging. + // this "fixes" itself when deselecting the window. + auto dpr = this->devicePixelRatio(); + if (dpr != 1) + { + painter.setTransform(QTransform::fromScale(1 / dpr, 1 / dpr)); + } + painter.fillRect(1, 1, this->realBounds_.width() - 2, + this->realBounds_.height() - 2, bg); + } } #endif } -bool BaseWindow::handleDPICHANGED(MSG *msg) -{ -#ifdef USEWINSDK - int dpi = HIWORD(msg->wParam); - - float _scale = dpi / 96.f; - - auto *prcNewWindow = reinterpret_cast(msg->lParam); - SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, - prcNewWindow->right - prcNewWindow->left, - prcNewWindow->bottom - prcNewWindow->top, - SWP_NOZORDER | SWP_NOACTIVATE); - - this->nativeScale_ = _scale; - this->updateScale(); - - return true; -#else - return false; -#endif -} - bool BaseWindow::handleSHOWWINDOW(MSG *msg) { #ifdef USEWINSDK @@ -883,16 +1008,6 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) return true; } - if (auto dpi = getWindowDpi(msg->hwnd)) - { - float currentScale = (float)dpi.value() / 96.F; - if (currentScale != this->nativeScale_) - { - this->nativeScale_ = currentScale; - this->updateScale(); - } - } - if (!this->shown_) { this->shown_ = true; @@ -906,14 +1021,12 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) if (!this->initalBounds_.isNull()) { - ::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(), - this->initalBounds_.y(), this->initalBounds_.width(), - this->initalBounds_.height(), - SWP_NOZORDER | SWP_NOACTIVATE); + this->setGeometry(this->initalBounds_); this->currentBounds_ = this->initalBounds_; } this->calcButtonsSizes(); + this->updateRealSize(); } return true; @@ -929,23 +1042,54 @@ bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result) #endif { #ifdef USEWINSDK - if (this->hasCustomWindowFrame()) + if (!this->hasCustomWindowFrame()) { - if (msg->wParam == TRUE) - { - // remove 1 extra pixel on top of custom frame - auto *ncp = reinterpret_cast(msg->lParam); - if (ncp) - { - ncp->lppos->flags |= SWP_NOREDRAW; - ncp->rgrc[0].top -= 1; - } - } + return false; + } + if (msg->wParam != TRUE) + { *result = 0; return true; } - return false; + + auto *params = reinterpret_cast(msg->lParam); + auto *r = ¶ms->rgrc[0]; + + WINDOWPLACEMENT wp; + wp.length = sizeof(WINDOWPLACEMENT); + this->isMaximized_ = GetWindowPlacement(msg->hwnd, &wp) != 0 && + (wp.showCmd == SW_SHOWMAXIMIZED); + + auto borders = windowBordersFor(msg->hwnd, this->isMaximized_); + r->left += borders.left; + r->top += borders.top; + r->right += borders.right; + r->bottom += borders.bottom; + + if (borders.left != 0 || borders.top != 0 || borders.right != 0 || + borders.bottom != 0) + { + // We added borders -> we changed the rect, so we can't return + // WVR_VALIDRECTS + *result = 0; + return true; + } + + // This is an attempt at telling Windows to not redraw (or at least to do a + // better job at redrawing) the window. There is a long list of tricks + // people tried to prevent this at + // https://stackoverflow.com/q/53000291/16300717 + // + // We set the source and destination rectangles to a 1x1 rectangle at the + // top left. Windows is instructed by WVR_VALIDRECTS to copy and preserve + // some parts of the window image. + QPoint fixed = {r->left, r->top}; + params->rgrc[1] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + params->rgrc[2] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + *result = WVR_VALIDRECTS; + + return true; #else return false; #endif @@ -962,28 +1106,11 @@ bool BaseWindow::handleSIZE(MSG *msg) } else if (this->hasCustomWindowFrame()) { - if (msg->wParam == SIZE_MAXIMIZED) - { - auto offset = - int(getWindowDpi(msg->hwnd).value_or(96) * 8 / 96); - - this->ui_.windowLayout->setContentsMargins(offset, offset, - offset, offset); - } - else - { - this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0); - } - this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED; if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->currentBounds_ = - QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); + this->currentBounds_ = this->geometry(); } this->useNextBounds_.stop(); @@ -993,6 +1120,12 @@ bool BaseWindow::handleSIZE(MSG *msg) // the minimize button, so we have to emulate it. this->ui_.titlebarButtons->leave(); } + + RECT real; + ::GetWindowRect(msg->hwnd, &real); + this->realBounds_ = + QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); } } return false; @@ -1006,11 +1139,7 @@ bool BaseWindow::handleMOVE(MSG *msg) #ifdef USEWINSDK if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->nextBounds_ = QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); - + this->nextBounds_ = this->geometry(); this->useNextBounds_.start(10); } #endif @@ -1024,31 +1153,37 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) #endif { #ifdef USEWINSDK - const LONG border_width = 8; // in pixels - RECT winrect; - GetWindowRect(msg->hwnd, &winrect); + const LONG borderWidth = 8; // in device independent pixels - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); + auto rect = this->rect(); - QPoint point(x - winrect.left, y - winrect.top); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint point(p.x, p.y); + point /= this->devicePixelRatio(); + + auto x = point.x(); + auto y = point.y(); if (this->hasCustomWindowFrame()) { *result = 0; - bool resizeWidth = minimumWidth() != maximumWidth(); - bool resizeHeight = minimumHeight() != maximumHeight(); + bool resizeWidth = + minimumWidth() != maximumWidth() && !this->isMaximized(); + bool resizeHeight = + minimumHeight() != maximumHeight() && !this->isMaximized(); if (resizeWidth) { // left border - if (x < winrect.left + border_width) + if (x < rect.left() + borderWidth) { *result = HTLEFT; } // right border - if (x >= winrect.right - border_width) + if (x >= rect.right() - borderWidth) { *result = HTRIGHT; } @@ -1056,12 +1191,12 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeHeight) { // bottom border - if (y >= winrect.bottom - border_width) + if (y >= rect.bottom() - borderWidth) { *result = HTBOTTOM; } // top border - if (y < winrect.top + border_width) + if (y < rect.top() + borderWidth) { *result = HTTOP; } @@ -1069,26 +1204,26 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeWidth && resizeHeight) { // bottom left corner - if (x >= winrect.left && x < winrect.left + border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMLEFT; } // bottom right corner - if (x < winrect.right && x >= winrect.right - border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMRIGHT; } // top left corner - if (x >= winrect.left && x < winrect.left + border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPLEFT; } // top right corner - if (x < winrect.right && x >= winrect.right - border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPRIGHT; } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index b9f21b08d..02101cb6d 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -75,7 +75,6 @@ public: bool applyLastBoundsCheck(); float scale() const override; - float qtFontScale() const; /// @returns true if the window is the top-most window. /// Either #setTopMost was called or the `TopMost` flag is set which overrides this @@ -132,7 +131,6 @@ private: void drawCustomWindowFrame(QPainter &painter); void onFocusLost(); - bool handleDPICHANGED(MSG *msg); bool handleSHOWWINDOW(MSG *msg); bool handleSIZE(MSG *msg); bool handleMOVE(MSG *msg); @@ -149,8 +147,6 @@ private: bool frameless_; bool shown_ = false; FlagsEnum flags_; - float nativeScale_ = 1; - bool isResizeFixing_ = false; bool isTopMost_ = false; struct { @@ -168,6 +164,7 @@ private: widgets::BoundsChecking lastBoundsCheckMode_ = widgets::BoundsChecking::Off; #ifdef USEWINSDK + void updateRealSize(); /// @brief Returns the HWND of this window if it has one /// /// A QWidget only has an HWND if it has been created. Before that, @@ -193,6 +190,10 @@ private: QTimer useNextBounds_; bool isNotMinimizedOrMaximized_{}; bool lastEventWasNcMouseMove_ = false; + /// The real bounds of the window as returned by + /// GetWindowRect. Used for drawing. + QRect realBounds_; + bool isMaximized_ = false; #endif pajlada::Signals::SignalHolder connections_; diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index 37bd9df36..1d3e7067f 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -88,23 +88,10 @@ void Label::paintEvent(QPaintEvent *) { QPainter painter(this); - qreal deviceDpi = -#ifdef Q_OS_WIN - this->devicePixelRatioF(); -#else - 1.0; -#endif - QFontMetrics metrics = getIApp()->getFonts()->getFontMetrics( - this->getFontStyle(), - this->scale() * 96.f / - std::max( - 0.01F, static_cast(this->logicalDpiX() * deviceDpi))); - painter.setFont(getIApp()->getFonts()->getFont( - this->getFontStyle(), - this->scale() * 96.f / - std::max( - 0.02F, static_cast(this->logicalDpiX() * deviceDpi)))); + this->getFontStyle(), this->scale()); + painter.setFont( + getIApp()->getFonts()->getFont(this->getFontStyle(), this->scale())); int offset = this->getOffset(); diff --git a/src/widgets/TooltipEntryWidget.cpp b/src/widgets/TooltipEntryWidget.cpp index 7ef7274e3..0f0ac5336 100644 --- a/src/widgets/TooltipEntryWidget.cpp +++ b/src/widgets/TooltipEntryWidget.cpp @@ -86,6 +86,7 @@ bool TooltipEntryWidget::refreshPixmap() this->attemptRefresh_ = true; return false; } + pixmap->setDevicePixelRatio(this->devicePixelRatio()); if (this->customImgWidth_ > 0 || this->customImgHeight_ > 0) { diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 62d459e22..8b56f4903 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -47,7 +47,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) this->resize(915, 600); this->themeChangedEvent(); - this->scaleChangedEvent(this->scale()); + QFile styleFile(":/qss/settings.qss"); + styleFile.open(QFile::ReadOnly); + QString stylesheet = QString::fromUtf8(styleFile.readAll()); + this->setStyleSheet(stylesheet); this->initUi(); this->addTabs(); @@ -396,25 +399,19 @@ void SettingsDialog::refresh() void SettingsDialog::scaleChangedEvent(float newDpi) { - QFile file(":/qss/settings.qss"); - file.open(QFile::ReadOnly); - QString styleSheet = QLatin1String(file.readAll()); - styleSheet.replace("", QString::number(int(14 * newDpi))); - styleSheet.replace("", QString::number(int(14 * newDpi))); + assert(newDpi == 1.F && + "Scaling is disabled for the settings dialog - its scale should " + "always be 1"); for (SettingsDialogTab *tab : this->tabs_) { - tab->setFixedHeight(int(30 * newDpi)); + tab->setFixedHeight(30); } - this->setStyleSheet(styleSheet); - if (this->ui_.tabContainerContainer) { - this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi)); + this->ui_.tabContainerContainer->setFixedWidth(150); } - - this->dpi_ = newDpi; } void SettingsDialog::themeChangedEvent() diff --git a/src/widgets/helper/Button.cpp b/src/widgets/helper/Button.cpp index 08dde78e1..057432666 100644 --- a/src/widgets/helper/Button.cpp +++ b/src/widgets/helper/Button.cpp @@ -8,26 +8,44 @@ #include #include -namespace chatterino { namespace { - // returns a new resized image or the old one if the size didn't change - auto resizePixmap(const QPixmap ¤t, const QPixmap resized, - const QSize &size) -> QPixmap +QSizeF deviceIndependentSize(const QPixmap &pixmap) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0) + return QSizeF(pixmap.width(), pixmap.height()) / pixmap.devicePixelRatio(); +#else + return pixmap.deviceIndependentSize(); +#endif +} + +/** + * Resizes a pixmap to a desired size. + * Does nothing if the target pixmap is already sized correctly. + * + * @param target The target pixmap. + * @param source The unscaled pixmap. + * @param size The desired device independent size. + * @param dpr The device pixel ratio of the target area. The size of the target in pixels will be `size * dpr`. + */ +void resizePixmap(QPixmap &target, const QPixmap &source, const QSize &size, + qreal dpr) +{ + if (deviceIndependentSize(target) == size) { - if (resized.size() == size) - { - return resized; - } - else - { - return current.scaled(size, Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - } + return; } + QPixmap resized = source; + resized.setDevicePixelRatio(dpr); + target = resized.scaled(size * dpr, Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); +} + } // namespace +namespace chatterino { + Button::Button(BaseWidget *parent) : BaseWidget(parent) { @@ -47,6 +65,12 @@ void Button::setMouseEffectColor(std::optional color) void Button::setPixmap(const QPixmap &_pixmap) { + // Avoid updates if the pixmap didn't change + if (_pixmap.cacheKey() == this->pixmap_.cacheKey()) + { + return; + } + this->pixmap_ = _pixmap; this->resizedPixmap_ = {}; this->update(); @@ -158,8 +182,8 @@ void Button::paintButton(QPainter &painter) QRect rect = this->rect(); - this->resizedPixmap_ = - resizePixmap(this->pixmap_, this->resizedPixmap_, rect.size()); + resizePixmap(this->resizedPixmap_, this->pixmap_, rect.size(), + this->devicePixelRatio()); int margin = this->height() < 22 * this->scale() ? 3 : 6; diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 819b7d0c0..ed636cca4 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -615,7 +615,7 @@ void ChannelView::scaleChangedEvent(float scale) if (this->goToBottom_) { - auto factor = this->qtFontScale(); + auto factor = this->scale(); #ifdef Q_OS_MACOS factor = scale * 80.F / std::max( @@ -703,8 +703,10 @@ void ChannelView::layoutVisibleMessages( { const auto &message = messages[i]; - redrawRequired |= message->layout(layoutWidth, this->scale(), flags, - this->bufferInvalidationQueued_); + redrawRequired |= message->layout( + layoutWidth, this->scale(), + this->scale() * static_cast(this->devicePixelRatio()), + flags, this->bufferInvalidationQueued_); y += message->getHeight(); } @@ -738,7 +740,10 @@ void ChannelView::updateScrollbar( { auto *message = messages[i].get(); - message->layout(layoutWidth, this->scale(), flags, false); + message->layout( + layoutWidth, this->scale(), + this->scale() * static_cast(this->devicePixelRatio()), flags, + false); h -= message->getHeight(); @@ -1720,9 +1725,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } else { - snapshot[i - 1]->layout(this->getLayoutWidth(), - this->scale(), this->getFlags(), - false); + snapshot[i - 1]->layout( + this->getLayoutWidth(), this->scale(), + this->scale() * + static_cast(this->devicePixelRatio()), + this->getFlags(), false); scrollFactor = 1; currentScrollLeft = snapshot[i - 1]->getHeight(); } @@ -1755,9 +1762,11 @@ void ChannelView::wheelEvent(QWheelEvent *event) } else { - snapshot[i + 1]->layout(this->getLayoutWidth(), - this->scale(), this->getFlags(), - false); + snapshot[i + 1]->layout( + this->getLayoutWidth(), this->scale(), + this->scale() * + static_cast(this->devicePixelRatio()), + this->getFlags(), false); scrollFactor = 1; currentScrollLeft = snapshot[i + 1]->getHeight(); diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index da04562fa..f7cb2b3a0 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -27,15 +27,6 @@ namespace chatterino { namespace { - qreal deviceDpi(QWidget *widget) - { -#ifdef Q_OS_WIN - return widget->devicePixelRatioF(); -#else - return 1.0; -#endif - } - // Translates the given rectangle by an amount in the direction to appear like the tab is selected. // For example, if location is Top, the rectangle will be translated in the negative Y direction, // or "up" on the screen, by amount. @@ -196,8 +187,8 @@ int NotebookTab::normalTabWidth() float scale = this->scale(); int width; - auto metrics = getIApp()->getFonts()->getFontMetrics( - FontStyle::UiTabs, float(qreal(this->scale()) * deviceDpi(this))); + QFontMetrics metrics = + getIApp()->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); if (this->hasXButton()) { @@ -439,11 +430,9 @@ void NotebookTab::paintEvent(QPaintEvent *) QPainter painter(this); float scale = this->scale(); - auto div = std::max(0.01f, this->logicalDpiX() * deviceDpi(this)); - painter.setFont( - getIApp()->getFonts()->getFont(FontStyle::UiTabs, scale * 96.f / div)); + painter.setFont(app->getFonts()->getFont(FontStyle::UiTabs, scale)); QFontMetrics metrics = - app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale * 96.f / div); + app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale); int height = int(scale * NOTEBOOK_TAB_HEIGHT); diff --git a/tests/src/MessageLayout.cpp b/tests/src/MessageLayout.cpp index ab9a294c9..8533b87b8 100644 --- a/tests/src/MessageLayout.cpp +++ b/tests/src/MessageLayout.cpp @@ -63,7 +63,7 @@ public: builder.append( std::make_unique(text, MessageElementFlag::Text)); this->layout = std::make_unique(builder.release()); - this->layout->layout(WIDTH, 1, MessageElementFlag::Text, false); + this->layout->layout(WIDTH, 1, 1, MessageElementFlag::Text, false); } MockApplication mockApplication;