Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/qt/guiutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,19 @@ namespace GUIUtil
* point size on first call so re-application on font/theme changes preserves it. */
void setStyledHtml(QTextEdit* widget, const QString& html);

//! Implementation details exposed only so qt/test can cover them directly.
namespace internal {
/** Return `font`'s size in points, converting from pixels when it was specified that way
* (e.g. a stylesheet's `font-size: Npx`). QFont stores exactly one of the two sizes and
* reports -1 for the other, so a pixel-sized font yields -1 from both pointSize() and
* pointSizeF() and must be converted rather than read directly.
*
* `dpi_y` is the target device's vertical logical DPI (QPaintDevice::logicalDpiY()).
* Returns std::nullopt when no usable size can be derived, which callers must handle:
* a font can carry no valid size at all, and `dpi_y` is not guaranteed to be positive. */
std::optional<double> effectivePointSize(const QFont& font, int dpi_y);
} // namespace internal

/**
* Determine default data directory for operating system.
*/
Expand Down
76 changes: 58 additions & 18 deletions src/qt/guiutil_font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#include <QFont>
#include <QFontDatabase>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QPointer>
#include <QScreen>
#include <QStringView>
#include <QTextBlock>
#include <QTextCharFormat>
Expand All @@ -26,6 +28,7 @@
#include <cmath>
#include <map>
#include <memory>
#include <optional>
#include <utility>

namespace {
Expand Down Expand Up @@ -148,11 +151,13 @@ std::unique_ptr<QFontDatabase> g_font_db{nullptr};
//! loadFonts stores the SystemDefault font in g_default_font to be able to reference it later again
std::unique_ptr<QFont> g_default_font{nullptr};

//! Font scaling information for Qt classes
std::map<std::string, int> mapClassFontUpdates{
{"QMenu", -1},
{"QMessageBox", -1},
{"QTipLabel", -1},
//! Font scaling information for Qt classes. The base size is captured on the first pass;
//! std::nullopt means "not captured yet", which a plain -1 could not express because that
//! is also what a pixel-sized font reports as its point size.
std::map<std::string, std::optional<double>> mapClassFontUpdates{
{"QMenu", std::nullopt},
{"QMessageBox", std::nullopt},
{"QTipLabel", std::nullopt},
};

//! Contains all widgets and its font attributes (weight, italic, size) with font changes due to GUIUtil::setFont
Expand Down Expand Up @@ -536,6 +541,25 @@ bool FontRegistry::IsValidWeight(const QFont::Weight& weight) const

namespace GUIUtil {

namespace internal {
std::optional<double> effectivePointSize(const QFont& font, int dpi_y)
{
// Both accessors return a non-positive sentinel when the size was given in the other
// unit, and a font with no usable size at all reports non-positive from both. Compare
// against 0 rather than -1: the exact sentinel is not guaranteed (a box-engine fallback
// yields values such as -0.72).
if (const double point_size{font.pointSizeF()}; point_size > 0) {
return point_size;
}
if (const int pixel_size{font.pixelSize()}; pixel_size > 0 && dpi_y > 0) {
// Mirrors Qt's own pixel-to-point conversion in QFontDatabase::load(), including
// its guard against a non-positive DPI.
return pixel_size * 72.0 / dpi_y;
}
return std::nullopt;
}
} // namespace internal

int defaultFontScale() { return DEFAULT_FONT_SCALE; }
int defaultFontSize() { return DEFAULT_FONT_SIZE; }
QString defaultFontFamily() { return DEFAULT_FONT.toString(); }
Expand Down Expand Up @@ -728,17 +752,28 @@ void updateFonts()
// Do not apply styling logic if ignored or handled separately
continue;
}
QFont font = w->font();
// A stylesheet rule such as `font-size: Npx` leaves the widget with a pixel-sized
// font, which reports no point size. Convert instead of assuming, and skip the
// widget outright if no size can be derived -- leaving one widget unscaled beats
// aborting, and substituting a default would resize a widget the stylesheet
// deliberately sized.
const std::optional<double> base_size{internal::effectivePointSize(font, w->logicalDpiY())};
if (!base_size) {
continue;
}
++nUpdatable;

QFont font = w->font();
assert(font.pointSize() > 0);
font.setFamily(qApp->font().family());
font.setWeight(g_font_registry.GetWeightNormal());
font.setStyleName(qApp->font().styleName());
font.setStyle(qApp->font().style());

// Insert/Get the default font size of the widget
auto itDefault = mapWidgetDefaultFontSizes.emplace(w, font.pointSize());
// Insert/Get the default font size of the widget. Seeded once per widget, so a
// later stylesheet re-apply cannot compound the scaling. Note this freezes a
// pixel-derived size at the DPI first seen; moving the window to a screen with a
// different DPI will not re-honour the stylesheet's pixel intent.
auto itDefault = mapWidgetDefaultFontSizes.emplace(w, *base_size);
Comment on lines +772 to +776

@coderabbitai coderabbitai Bot Jul 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file and relevant symbols =="
git ls-files | grep -F 'src/qt/guiutil_font.cpp' || true
wc -l src/qt/guiutil_font.cpp
fd -i 'guiutil_font.cpp|guiutil_font.h' .

echo "== outline around relevant functions =="
ast-grep outline src/qt/guiutil_font.cpp --match ApplyStyleSheet --view expanded || true
ast-grep outline src/qt/guiutil_font.cpp --match internal --view expanded || true

echo "== relevant slices =="
sed -n '720,860p' src/qt/guiutil_font.cpp

echo "== definitions map variables =="
rg -n "mapWidgetDefaultFontSizes|mapClassFontUpdates|effectivePointSize|GetScaledFontSize|logicalDpiY|screenDpi|logicalDotsPerInch" src/qt/guiutil_font.cpp src/qt -g '*.cpp' -g '*.h' | head -200

Repository: dashpay/dash

Length of output: 1910


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect surrounding code in full relevant section =="
sed -n '1,260p' src/qt/guiutil_font.cpp
echo "== next sections =="
sed -n '560,720p' src/qt/guiutil_font.cpp
sed -n '860,970p' src/qt/guiutil_font.cpp
sed -n '970,1080p' src/qt/guiutil_font.cpp

Repository: dashpay/dash

Length of output: 1910


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo status =="
git rev-parse --show-toplevel 2>/dev/null || true
git status --short 2>/dev/null | head -50 || true

echo "== locate relevant files =="
git ls-files 2>/dev/null | grep -- 'src/qt/guiutil_font\.\(cpp|h\)$' || true

echo "== inspect relevant file sizes =="
for f in $(git ls-files 2>/dev/null | grep -- 'src/qt/guiutil_font\.cpp$' || true); do
  wc -l "$f"
done

echo "== relevant definitions and slices =="
fs="$(git ls-files 2>/dev/null | grep -- 'src/qt/guiutil_font\.cpp$' | head -1 || true)"
if [ -n "${fs:-}" ]; then
  sed -n '1,260p' "$fs"
  echo "///// 560-720"
  sed -n '560,720p' "$fs"
  echo "///// 720-860"
  sed -n '720,860p' "$fs"
  echo "///// 860-1080"
  sed -n '860,1080p' "$fs"
fi

echo "== searches =="
rg -n "mapWidgetDefaultFontSizes|mapClassFontUpdates|effectivePointSize|GetScaledFontSize|logicalDpiY|logicalDotsPerInch|setPointSize|PixelMetric" src/qt/guiutil_font.cpp src/qt -g '*.cpp' -g '*.h' 2>/dev/null || true

Repository: dashpay/dash

Length of output: 1910


🌐 Web query:

Qt documentation QFont isPixel logicalDotsPerInch logicalDotsPerInchY

💡 Result:

In Qt, logical DPI (dots per inch) is a metric used to scale user interface elements and convert font point sizes into pixel sizes, ensuring consistent physical size across different displays [1][2][3]. The key properties and functions related to this concept are: QScreen Properties The QScreen class provides three read-only properties for logical DPI [1][4]: - logicalDotsPerInchX: Returns the horizontal logical DPI [1][3]. - logicalDotsPerInchY: Returns the vertical logical DPI [1][3]. - logicalDotsPerInch: A convenience property that returns the average of the horizontal and vertical logical DPI values [1][3]. QPaintDevice Functions The QPaintDevice class (the base class for objects like QWidget and QPixmap) provides equivalent methods [5][6][2]: - logicalDpiX: Returns the horizontal resolution in dots per inch [5][6]. - logicalDpiY: Returns the vertical resolution in dots per inch [5][6]. Key Distinctions and Usage - Purpose: Logical DPI is used by Qt to calculate UI scaling and font sizes [1][2][7]. It may differ from physical DPI, which represents the screen's actual hardware pixel density [1][8][2]. - Customization: Users or desktop environments can sometimes adjust logical DPI settings to globally control the size of UI elements and text [1][8]. - High DPI Scaling: When high-DPI scaling is active, Qt calculates the appropriate scale factor (device pixel ratio) using the logical DPI relative to a base DPI (typically 96) [9][7]. - QFont Interaction: While QFont itself does not directly hold DPI properties, it relies on the system's logical DPI to interpret point sizes correctly during font matching and rendering [10][11][3]. In modern Qt development, relying on the QScreen methods is the preferred way to query these values for display-specific information, as QPaintDevice methods are often used for general device resolution queries [2].

Citations:


🌐 Web query:

Qt QFont documentation logicalDpiY QtQPA logicalDotsPerInchY documentation source code

💡 Result:

In Qt, logicalDpiY (and its associated property logicalDotsPerInchY) represents the vertical resolution of a device in dots per inch, which is specifically used for calculating font sizes [1][2][3][4]. Key Technical Details: 1. QPaintDevice::logicalDpiY: This function is a member of the QPaintDevice class and returns the device's vertical DPI [1][2]. It is used to ensure that fonts are rendered at the correct physical size regardless of the display's actual pixel density [1][2]. If the logical DPI differs from the physical DPI, the QPaintEngine is responsible for handling the mapping [1][2]. 2. QScreen::logicalDotsPerInchY: This property (accessed via the method of the same name) provides the vertical logical DPI for a specific screen [3][4]. It is primarily used to convert font point sizes into pixel sizes [3][4]. 3. Implementation and Usage: - When a QFont is constructed for a specific QPaintDevice, Qt uses the device's logicalDpiY to initialize the font's internal DPI settings, ensuring the font metrics are appropriate for that device [5][6][7]. - If no specific device is provided, Qt falls back to a default DPI (often retrieved via internal helper functions like qt_defaultDpiY) [5][6][7]. - In the Qt source code (e.g., in qfont.cpp), these values are frequently accessed to determine the scaling factor for text rendering [5][6][7]. In summary, logicalDpiY is a fundamental metric in Qt's font rendering pipeline that bridges the gap between abstract point sizes and concrete pixel dimensions [1][3][4].

Citations:


🌐 Web query:

"QFont" "logicalDpiY" "dpiY" "Qt" "isPixel" documentation

💡 Result:

In Qt, logicalDpiY and QFont size settings serve different purposes regarding UI scaling and device independence [1][2]. logicalDpiY (from QPaintDevice) The logicalDpiY function returns the vertical resolution of a paint device in dots per inch (DPI), which is used by Qt to calculate font sizes and scale UI elements [3][1]. It represents the resolution used by the operating system or Qt framework for display scaling, intended to ensure that elements maintain a consistent physical appearance across different monitors [1][2]. QFont and Font Sizes QFont supports two primary ways of defining font size: point size and pixel size [4][5]. 1. Point Size (Recommended): When you use setPointSize or setPointSizeF, the font size is defined in points (1/72 of an inch) [1]. This is a device-independent measurement [5]. Qt converts this point size into a pixel size based on the device's logical DPI [1][2]. Because this conversion accounts for the current logical DPI, point-sized fonts scale automatically when the system DPI or user scaling settings change [1][6]. 2. Pixel Size (Device Dependent): When you use setPixelSize, the font size is defined explicitly in pixels [7][5]. This makes the font device-dependent because it bypasses the logical DPI scaling calculation [5][6]. A pixel-sized font will generally remain the same number of pixels tall regardless of the DPI, which can cause it to appear too small on high-resolution (high-DPI) displays if not handled manually [1][6]. Key Takeaways - Use setPointSize for most applications to ensure font scaling behaves correctly with the system's logical DPI [5][1]. - logicalDpiY is a metric used for scaling, not a property you typically modify for individual font objects [1]. - If you use setPixelSize, you are responsible for manually adjusting the font size if you want it to scale with the display resolution [5][1]. For further details, refer to the QPaintDevice::logicalDpiY and QFont documentation [3][4].

Citations:


Preserve pixel-sized cache entries in their original unit.

Both caches store the first converted point size instead of the original pixel size. A pixel font captured at one DPI is then reused on a different DPI as the same point value, so stylesheet text can shrink or grow unintentionally after a screen/DPI transition. Keep the original pixel size for these Qt pixel-font entries and rederive the scaled point size from the relevant current DPI for each refresh.

📍 Affects 1 file
  • src/qt/guiutil_font.cpp#L772-L776 (this comment)
  • src/qt/guiutil_font.cpp#L807-L820
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qt/guiutil_font.cpp` around lines 772 - 776, Update both font-cache sites
in src/qt/guiutil_font.cpp: lines 772-776 near mapWidgetDefaultFontSizes and
lines 807-820 in the companion cache path. Store the original pixel-sized value
for Qt pixel-font entries, rather than the first DPI-converted point size, and
on each refresh recompute the point size using the current relevant DPI;
preserve existing behavior for non-pixel font entries.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. This identifies a real but explicitly documented limitation of the merged fix, rather than the original abort regression.

The per-widget cache intentionally freezes the first usable base size so repeated updateFonts() passes cannot compound scaling; the final code calls out that a pixel-derived size will therefore not be re-derived after a later per-screen DPI transition. Preserving pixel intent across such transitions would require a unit-aware cache (pixel vs. point), current-screen DPI conversion on each refresh, and focused screen/DPI-change coverage.

That broader behavior was not folded into the already-merged crash fix. No further change can be made on this PR; this comment remains useful follow-up guidance.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thepastaclaw, agreed. The finding is valid follow-up guidance, but the documented first-DPI cache behavior is an intentional trade-off in the merged crash-regression fix—not a reason to reopen #7465.

Would you like me to open a GitHub issue for unit-aware pixel/point font caching, DPI-transition re-conversion, and focused multi-screen/DPI test coverage?

🐇🔍


✏️ Learnings added
Learnt from: thepastaclaw
Repo: dashpay/dash PR: 7465
File: src/qt/guiutil_font.cpp:772-776
Timestamp: 2026-07-29T20:39:54.924Z
Learning: In `src/qt/guiutil_font.cpp`, `GUIUtil::updateFonts()` intentionally caches the first derived widget base font size to prevent scaling from compounding on repeated refreshes. Consequently, a pixel-sized stylesheet font converted at its first observed DPI is not re-derived when the widget later moves to a screen with a different DPI. Preserving pixel intent across DPI transitions requires a unit-aware cache, conversion using the current relevant DPI on each refresh, and dedicated screen/DPI-change tests; this behavior was deliberately out of scope for the crash fix merged in Dash pull request `#7465`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


auto it = mapFontUpdates.find(w);
if (it != mapFontUpdates.end()) {
Expand Down Expand Up @@ -769,13 +804,20 @@ void updateFonts()
it.first->setFont(it.second);
}

// Scale the global font size for the classes in the map below
// Scale the global font size for the classes in the map below. These fonts belong to no
// widget, so the primary screen supplies the DPI for any pixel-to-point conversion.
const QScreen* primary_screen{QGuiApplication::primaryScreen()};
const int screen_dpi_y{primary_screen ? qRound(primary_screen->logicalDotsPerInchY()) : 0};
for (auto& it : mapClassFontUpdates) {
QFont fontClass = qApp->font(it.first.c_str());
if (it.second == -1) {
it.second = fontClass.pointSize();
if (!it.second) {
// Leave the entry uncaptured and retry on the next pass if the size is unusable.
it.second = internal::effectivePointSize(fontClass, screen_dpi_y);
if (!it.second) {
continue;
}
}
double dSize = g_font_registry.GetScaledFontSize(it.second);
double dSize = g_font_registry.GetScaledFontSize(*it.second);
if (fontClass.pointSizeF() != dSize) {
fontClass.setPointSizeF(dSize);
qApp->setFont(fontClass, it.first.c_str());
Expand Down Expand Up @@ -825,11 +867,9 @@ void setStyledHtml(QTextEdit* widget, const QString& html)
base_size = it->second.base_size;
it->second.html = html;
} else {
// First registration, capture the widget's native font size
double widget_size{widget->font().pointSizeF()};
if (widget_size > 0) {
base_size = widget_size;
}
// First registration, capture the widget's native font size, converting it when the
// widget was sized in pixels. Falls back to DEFAULT_FONT_SIZE if no size is usable.
base_size = internal::effectivePointSize(widget->font(), widget->logicalDpiY()).value_or(base_size);
mapTextEditStyleUpdates[widget] = {html, base_size};
}
setFontBodyHTML(widget, html, base_size);
Expand Down
68 changes: 68 additions & 0 deletions src/qt/test/optiontests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
#include <test/util/setup_common.h>
#include <util/system.h>

#include <QApplication>
#include <QFont>
#include <QLabel>
#include <QSettings>
#include <QTest>
#include <QWidget>

#include <univalue.h>

Expand Down Expand Up @@ -132,3 +136,67 @@ void OptionTests::extractFilter()
filter = QString("Image (*.png *.jpg)");
QCOMPARE(GUIUtil::ExtractFirstSuffixFromFilter(filter), "png");
}

void OptionTests::effectivePointSize()
{
using GUIUtil::internal::effectivePointSize;

// A point-sized font reports its size directly, fractions included, whatever the DPI.
QFont point_font;
point_font.setPointSizeF(12.5);
QCOMPARE(effectivePointSize(point_font, 96).value_or(0), 12.5);
QCOMPARE(effectivePointSize(point_font, 72).value_or(0), 12.5);

// A pixel-sized font carries no point size and must be converted using the target DPI.
QFont pixel_font;
pixel_font.setPixelSize(17);
QVERIFY(pixel_font.pointSizeF() <= 0);
QCOMPARE(effectivePointSize(pixel_font, 96).value_or(0), 17 * 72.0 / 96);
QCOMPARE(effectivePointSize(pixel_font, 144).value_or(0), 8.5);
// At 72 DPI points and pixels coincide; pinned so the identity is deliberate rather
// than an accident of whichever DPI the host happens to report.
QCOMPARE(effectivePointSize(pixel_font, 72).value_or(0), 17.0);

// A non-positive DPI cannot yield a conversion factor, so the pixel size is unusable
// even though it is valid. QWidget::logicalDpiY() is not guaranteed to be positive.
QVERIFY(!effectivePointSize(pixel_font, 0).has_value());
QVERIFY(!effectivePointSize(pixel_font, -1).has_value());

// The remaining branch -- neither size usable -- is guarded but not asserted here: Qt
// rejects non-positive sizes in the setters, and once a QGuiApplication exists (as it
// does in this binary) every QFont is handed a valid default point size. The state is
// still reachable in production, e.g. a font engine that populates no size at all, so
// the helper compares against 0 rather than trusting any particular sentinel.
}

void OptionTests::updateFontsWithPixelSizedWidget()
{
if (QApplication::platformName() == "minimal") {
QSKIP("AppTests cannot initialize fonts with the 'minimal' platform plugin.");
}

// updateFonts() is a no-op until loadFonts() has run, and loadFonts() is process-global,
// non-idempotent state owned by AppTests. Treat missing initialization as a failure on
// supported platforms so the regression test cannot pass without exercising updateFonts().
QVERIFY2(GUIUtil::fontsLoaded(),
"GUIUtil::loadFonts() must succeed in AppTests::appTests() before OptionTests run.");
Comment on lines +181 to +182

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate this regression when AppTests skips font setup

On macOS the Qt test binary forces QT_QPA_PLATFORM=minimal before running AppTests and then OptionTests, but AppTests::appTests() returns before GUIUtil::loadFonts() on that platform. In that default macOS test context this new assertion fails before exercising the regression body, so test_dash-qt is broken unless this test also skips under the same condition or initializes the font state itself.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the final merged head (67d25da616a). OptionTests::updateFontsWithPixelSizedWidget() now calls QSKIP(...) when the platform plugin is minimal, and on supported platforms it requires GUIUtil::fontsLoaded() before exercising the regression path. The final CI matrix, including the macOS build, passed.


QWidget host;
QLabel* label{new QLabel(&host)};
QFont pixel_font{label->font()};
pixel_font.setPixelSize(17);
label->setFont(pixel_font);
QVERIFY(label->font().pointSizeF() <= 0);

// The pre-fix code asserted pointSize() > 0 here and aborted the process. The widget must
// now be swept normally and end up with a usable point size. The exact value depends on
// the host DPI, so the arithmetic is pinned in effectivePointSize() above instead.
GUIUtil::updateFonts();
const double scaled_size{label->font().pointSizeF()};
QVERIFY(scaled_size > 0);

// The size is cached per widget on the first sweep, so repeated passes must not compound
// it -- the defect that makes the cache load-bearing rather than an optimisation.
GUIUtil::updateFonts();
QCOMPARE(label->font().pointSizeF(), scaled_size);
}
2 changes: 2 additions & 0 deletions src/qt/test/optiontests.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ private Q_SLOTS:
void integerGetArgBug();
void parametersInteraction();
void extractFilter();
void effectivePointSize();
void updateFontsWithPixelSizedWidget();

private:
interfaces::Node& m_node;
Expand Down
Loading