Qt: Get window sizes from OS instead of scaling Qt size

Qt's device independent sizes are a lossy transformation.

This stops our sizes from being potentially off by one with the "real"
window size.

Too bad it can't be done properly on Linux, see comment. Don't care.
This commit is contained in:
Stenzek
2025-11-13 23:24:57 +10:00
parent d1f26eef3b
commit 35e3d9a4d1
8 changed files with 89 additions and 46 deletions

View File

@@ -1,12 +1,17 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#ifndef __APPLE__
#error This file should only be included when compiling for MacOS.
#endif
#include "types.h"
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
class Error;
@@ -40,4 +45,7 @@ std::optional<std::string> GetNonTranslocatedBundlePath();
/// Launch the given application once this one quits
bool DelayedLaunch(std::string_view file, std::span<const std::string_view> args = {});
/// Returns the size of a NSView in pixels.
std::optional<std::pair<int, int>> GetViewSizeInPixels(const void* view);
} // namespace CocoaTools

View File

@@ -145,6 +145,19 @@ bool CocoaTools::DelayedLaunch(std::string_view file, std::span<const std::strin
}
}
std::optional<std::pair<int, int>> CocoaTools::GetViewSizeInPixels(const void* view)
{
std::optional<std::pair<int, int>> ret;
if (view)
{
NSView* nsview = (__bridge NSView*)view;
const NSSize size = [nsview convertSizeToBacking:nsview.frame.size];
ret = std::make_pair(static_cast<int>(size.width), static_cast<int>(size.height));
}
return ret;
}
void Y_OnAssertFailed(const char* szMessage, const char* szFunction, const char* szFile, unsigned uLine)
{
if (![NSThread isMainThread])

View File

@@ -3,9 +3,12 @@
#include "threading.h"
#include "assert.h"
#include "cocoa_tools.h"
#include "log.h"
#ifdef __APPLE__
#include "cocoa_tools.h"
#endif
#include <memory>
#include <utility>

View File

@@ -49,25 +49,12 @@ DisplayWidget::DisplayWidget(QWidget* parent) : QWidget(parent)
DisplayWidget::~DisplayWidget() = default;
int DisplayWidget::scaledWindowWidth() const
{
return std::max(
static_cast<int>(std::ceil(static_cast<qreal>(width()) * QtUtils::GetDevicePixelRatioForWidget(this))), 1);
}
int DisplayWidget::scaledWindowHeight() const
{
return std::max(
static_cast<int>(std::ceil(static_cast<qreal>(height()) * QtUtils::GetDevicePixelRatioForWidget(this))), 1);
}
std::optional<WindowInfo> DisplayWidget::getWindowInfo(RenderAPI render_api, Error* error)
{
std::optional<WindowInfo> ret = QtUtils::GetWindowInfoForWidget(this, render_api, error);
if (ret.has_value())
{
m_last_window_width = ret->surface_width;
m_last_window_height = ret->surface_height;
m_last_window_size = QSize(ret->surface_width, ret->surface_height);
m_last_window_scale = ret->surface_scale;
}
return ret;
@@ -363,18 +350,14 @@ bool DisplayWidget::event(QEvent* event)
QWidget::event(event);
const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this);
const u32 scaled_width =
static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(width()) * dpr)), 1));
const u32 scaled_height =
static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(height()) * dpr)), 1));
const QSize size = QtUtils::GetPixelSizeForWidget(this, dpr);
// avoid spamming resize events for paint events (sent on move on windows)
if (m_last_window_width != scaled_width || m_last_window_height != scaled_height || m_last_window_scale != dpr)
if (m_last_window_size != size || m_last_window_scale != dpr)
{
m_last_window_width = scaled_width;
m_last_window_height = scaled_height;
m_last_window_size = size;
m_last_window_scale = dpr;
emit windowResizedEvent(scaled_width, scaled_height, static_cast<float>(dpr));
emit windowResizedEvent(size.width(), size.height(), static_cast<float>(dpr));
}
updateCenterPos();
@@ -607,21 +590,17 @@ bool AuxiliaryDisplayWidget::event(QEvent* event)
QWidget::event(event);
const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this);
const u32 scaled_width =
static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(width()) * dpr)), 1));
const u32 scaled_height =
static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(height()) * dpr)), 1));
const QSize size = QtUtils::GetPixelSizeForWidget(this, dpr);
// avoid spamming resize events for paint events (sent on move on windows)
if (m_last_window_width != scaled_width || m_last_window_height != scaled_height || m_last_window_scale != dpr)
if (m_last_window_size != size || m_last_window_scale != dpr)
{
m_last_window_width = scaled_width;
m_last_window_height = scaled_height;
m_last_window_size = size;
m_last_window_scale = dpr;
g_emu_thread->queueAuxiliaryRenderWindowInputEvent(
m_userdata, Host::AuxiliaryRenderWindowEvent::Resized,
Host::AuxiliaryRenderWindowEventParam{.uint_param = scaled_width},
Host::AuxiliaryRenderWindowEventParam{.uint_param = scaled_height},
Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast<u32>(size.width())},
Host::AuxiliaryRenderWindowEventParam{.uint_param = static_cast<u32>(size.height())},
Host::AuxiliaryRenderWindowEventParam{.float_param = static_cast<float>(dpr)});
}

View File

@@ -30,9 +30,6 @@ public:
QPaintEngine* paintEngine() const override;
int scaledWindowWidth() const;
int scaledWindowHeight() const;
std::optional<WindowInfo> getWindowInfo(RenderAPI render_api, Error* error);
void updateRelativeMode(bool enabled);
@@ -67,7 +64,7 @@ private:
std::vector<int> m_keys_pressed_with_modifiers;
u32 m_last_window_width = 0;
QSize m_last_window_size;
u32 m_last_window_height = 0;
qreal m_last_window_scale = 1;
@@ -109,8 +106,7 @@ protected:
private:
void* m_userdata = nullptr;
u32 m_last_window_width = 0;
u32 m_last_window_height = 0;
QSize m_last_window_size;
qreal m_last_window_scale = 1;
bool m_destroying = false;
};

View File

@@ -458,7 +458,7 @@ void GameListModel::createPlaceholderImage(QImage& image, const QImage& placehol
painter.setFont(font);
const int margin = static_cast<int>(30.0f * scale);
const QSize unscaled_size = image.deviceIndependentSize().toSize();
const QSize unscaled_size = QtUtils::GetDeviceIndependentSize(image.size(), image.devicePixelRatio());
const QRect text_rc(margin, margin, static_cast<int>(static_cast<float>(unscaled_size.width() - margin - margin)),
static_cast<int>(static_cast<float>(unscaled_size.height() - margin - margin)));
@@ -1383,7 +1383,7 @@ public:
const QRect& r = option.rect;
const QPixmap pix = qvariant_cast<QPixmap>(index.data(Qt::DecorationRole));
const QSize pix_size = pix.deviceIndependentSize().toSize();
const QSize pix_size = QtUtils::GetDeviceIndependentSize(pix.size(), pix.devicePixelRatio());
// draw pixmap at center of item
const QPoint p((r.width() - pix_size.width()) / 2, (r.height() - pix_size.height()) / 2);
@@ -1435,7 +1435,7 @@ public:
const QPixmap& icon = (num_achievements > 0) ? (mastered ? m_model->getMasteredAchievementsPixmap() :
m_model->getHasAchievementsPixmap()) :
m_model->getNoAchievementsPixmap();
const QSize icon_size = icon.deviceIndependentSize().toSize();
const QSize icon_size = QtUtils::GetDeviceIndependentSize(icon.size(), icon.devicePixelRatio());
painter->drawPixmap(r.topLeft() + QPoint(5, (r.height() - icon_size.height() + 2) / 2), icon);
r.setLeft(r.left() + 12 + icon_size.width());
@@ -1515,7 +1515,7 @@ public:
const QRect& r = option.rect;
const QPixmap pix = m_frame_pixmaps[m_current_frame];
const QSize pix_size = pix.deviceIndependentSize().toSize();
const QSize pix_size = QtUtils::GetDeviceIndependentSize(pix.size(), pix.devicePixelRatio());
// draw pixmap at center of item
const QPoint p((r.width() - pix_size.width()) / 2, (r.height() - pix_size.height()) / 2);
@@ -1633,7 +1633,7 @@ public:
return;
const QPixmap& pix = m_model->getCoverForEntry(ge);
const QSize pix_size = pix.deviceIndependentSize().toSize();
const QSize pix_size = QtUtils::GetDeviceIndependentSize(pix.size(), pix.devicePixelRatio());
const QSize cover_size = m_model->getCoverArtSize();
// draw pixmap at center of item
@@ -1920,7 +1920,7 @@ void GameListWidget::updateBackground(bool reload_image)
}
QImage scaled_image = m_background_image;
resizeAndPadImage(&scaled_image, QSizeF(m_ui.stack->size() * devicePixelRatio()).toSize(), true, true);
resizeAndPadImage(&scaled_image, QtUtils::ApplyDevicePixelRatioToSize(m_ui.stack->size(), m_model->getDevicePixelRatio()), true, true);
QPalette new_palette = qApp->palette(m_ui.stack);
new_palette.setBrush(QPalette::Window, QPixmap::fromImage(scaled_image));

View File

@@ -40,6 +40,7 @@
#if defined(_WIN32)
#include "common/windows_headers.h"
#elif defined(__APPLE__)
#include "common/cocoa_tools.h"
#include "common/thirdparty/usb_key_code_data.h"
#else
#include <qpa/qplatformnativeinterface.h>
@@ -434,6 +435,18 @@ void QtUtils::ResizeSharpBilinear(QImage& pm, int size, int base_size)
ResizeSharpBilinearT(pm, size, base_size);
}
QSize QtUtils::ApplyDevicePixelRatioToSize(const QSize& size, qreal device_pixel_ratio)
{
return QSize(static_cast<int>(std::ceil(static_cast<qreal>(size.width()) * device_pixel_ratio)),
static_cast<int>(std::ceil(static_cast<qreal>(size.height()) * device_pixel_ratio)));
}
QSize QtUtils::GetDeviceIndependentSize(const QSize& size, qreal device_pixel_ratio)
{
return QSize(std::max(static_cast<int>(std::ceil(static_cast<qreal>(size.width()) / device_pixel_ratio)), 1),
std::max(static_cast<int>(std::ceil(static_cast<qreal>(size.height()) / device_pixel_ratio)), 1));
}
qreal QtUtils::GetDevicePixelRatioForWidget(const QWidget* widget)
{
const QScreen* screen_for_ratio = widget->screen();
@@ -443,6 +456,27 @@ qreal QtUtils::GetDevicePixelRatioForWidget(const QWidget* widget)
return screen_for_ratio ? screen_for_ratio->devicePixelRatio() : static_cast<qreal>(1);
}
QSize QtUtils::GetPixelSizeForWidget(const QWidget* widget, qreal device_pixel_ratio)
{
// Why this nonsense? Qt's device independent sizes are integer, and fractional scaling is lossy.
// We can't get back the "real" size of the window. So we have to platform natively query the actual client size.
#if defined(_WIN32)
if (RECT rc; GetClientRect(reinterpret_cast<HWND>(widget->winId()), &rc))
return QSize(static_cast<int>(rc.right - rc.left), static_cast<int>(rc.bottom - rc.top));
#elif defined(__APPLE__)
if (std::optional<std::pair<int, int>> size =
CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(widget->winId())))
{
return QSize(size->first, size->second);
}
#endif
// On Linux, fuck you, enjoy round trip to the X server, and on Wayland you can't query it in the first place...
// I ain't dealing with this crap OS. Enjoy your mismatched sizes and shit experience.
return ApplyDevicePixelRatioToSize(widget->size(), (device_pixel_ratio < 1) ? GetDevicePixelRatioForWidget(widget) :
device_pixel_ratio);
}
std::optional<WindowInfo> QtUtils::GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error)
{
WindowInfo wi;
@@ -489,8 +523,9 @@ std::optional<WindowInfo> QtUtils::GetWindowInfoForWidget(QWidget* widget, Rende
#endif
const qreal dpr = GetDevicePixelRatioForWidget(widget);
wi.surface_width = static_cast<u16>(static_cast<qreal>(widget->width()) * dpr);
wi.surface_height = static_cast<u16>(static_cast<qreal>(widget->height()) * dpr);
const QSize size = GetPixelSizeForWidget(widget, dpr);
wi.surface_width = static_cast<u16>(size.width());
wi.surface_height = static_cast<u16>(size.height());
wi.surface_scale = static_cast<float>(dpr);
// Query refresh rate, we need it for sync.

View File

@@ -144,9 +144,18 @@ QIcon GetIconForLanguage(std::string_view language_name);
void ResizeSharpBilinear(QPixmap& pm, int size, int base_size);
void ResizeSharpBilinear(QImage& pm, int size, int base_size);
/// Applies the device pixel ratio to the given size, giving the size in pixels.
QSize ApplyDevicePixelRatioToSize(const QSize& size, qreal device_pixel_ratio);
/// Removes the device pixel ratio from the given size, giving the size in device-independent units.
QSize GetDeviceIndependentSize(const QSize& size, qreal device_pixel_ratio);
/// Returns the pixel ratio/scaling factor for a widget.
qreal GetDevicePixelRatioForWidget(const QWidget* widget);
/// Returns the pixel size (real geometry) for a widget. DPR can be passed to avoid re-querying when needed.
QSize GetPixelSizeForWidget(const QWidget* widget, qreal device_pixel_ratio = -1);
/// Returns the common window info structure for a Qt widget.
std::optional<WindowInfo> GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error = nullptr);