dpi_scaling_round2: a bit of research on Qt6's hiDPI support #49

Open
goodboy wants to merge 4 commits from dpi_scaling_round2 into dpi-font-auto-calc
3 changed files with 109 additions and 21 deletions

View File

@ -184,22 +184,25 @@ class DpiAwareFont:
self._font_inches = inches self._font_inches = inches
font_size = math.floor(inches * pdpi) font_size = math.floor(inches * pdpi)
log.debug( ftype: str = f'{type(self)!r}'
f"screen:{screen.name()}\n" log.info(
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n" f'screen: {screen.name()}\n'
f"\nOur best guess font size is {font_size}\n" f'pDPI: {pdpi!r}\n'
f'lDPI: {ldpi!r}\n'
f'scale: {scale!r}\n'
f'{ftype}._font_inches={self._font_inches!r}\n'
f'\n'
f"Our best guess for an auto-font-size is,\n"
f'font_size: {font_size!r}\n'
) )
# apply the size # apply the size
self._set_qfont_px_size(font_size) self._set_qfont_px_size(font_size)
def boundingRect(self, value: str) -> QtCore.QRectF: def boundingRect(self, value: str) -> QtCore.QRectF:
if (screen := self.screen) is None:
screen = self.screen
if screen is None:
raise RuntimeError("You must call .configure_to_dpi() first!") raise RuntimeError("You must call .configure_to_dpi() first!")
unscaled_br = self._qfm.boundingRect(value) unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value)
return QtCore.QRectF( return QtCore.QRectF(
0, 0,
0, 0,

View File

@ -0,0 +1,64 @@
#!env xonsh
'''
Compute the pxs-per-inch (PPI) naively for the local DE.
NOTE, currently this only supports the `sway`-TWM on wayland.
!TODO!
- [ ] support Xorg (and possibly other OSs as well?
- [ ] conver this to pure py code, dropping the `.xsh` specifics
instead for `subprocess` API calls?
- [ ] possibly unify all this with `./qt_screen_info.py` as part of
a "PPI config wizard" or something, but more then likely we'll
have lib-ified version inside modden/piker by then?
'''
import math
import json
# XXX, xonsh part using "subprocess mode"
disp_infos: list[dict] = json.loads($(wlr-randr --json))
lappy: dict = disp_infos[0]
dims: dict[str, int] = lappy['physical_size']
w_cm: int = dims['width']
h_cm: int = dims['height']
# cm per inch
cpi: float = 25.4
# compute "diagonal" size (aka hypot)
diag_inches: float = math.sqrt((h_cm/cpi)**2 + (w_cm/cpi)**2)
# compute reso-hypot / inches-hypot
hi_res: dict[str, float|bool] = lappy['modes'][0]
w_px: int = hi_res['width']
h_px: int = hi_res['height']
diag_pxs: float = math.sqrt(h_px**2 + w_px**2)
unscaled_ppi: float = diag_pxs/diag_inches
# retrieve TWM info on the display (including scaling info)
sway_disp_info: dict = json.loads($(swaymsg -r -t get_outputs))[0]
scale: float = sway_disp_info['scale']
print(
f'output: {sway_disp_info["name"]!r}\n'
f'--- DIMENSIONS ---\n'
f'w_cm: {w_cm!r}\n'
f'h_cm: {h_cm!r}\n'
f'w_px: {w_px!r}\n'
f'h_cm: {h_px!r}\n'
f'\n'
f'--- DIAGONALS ---\n'
f'diag_inches: {diag_inches!r}\n'
f'diag_pxs: {diag_pxs!r}\n'
f'\n'
f'--- PPI-related-info ---\n'
f'(DE reported) scale: {scale!r}\n'
f'unscaled PPI: {unscaled_ppi!r}\n'
f'|_ =sqrt(h_px**2 + w_px**2) / sqrt(h_in**2 + w_in**2)\n'
f'scaled PPI: {unscaled_ppi/scale!r}\n'
f'|_ =unscaled_ppi/scale\n'
)

View File

@ -31,8 +31,8 @@ Resource list for mucking with DPIs on multiple screens:
- https://doc.qt.io/qt-5/qguiapplication.html#screenAt - https://doc.qt.io/qt-5/qguiapplication.html#screenAt
''' '''
import os
from pyqtgraph import QtGui
from PyQt6 import ( from PyQt6 import (
QtCore, QtCore,
QtWidgets, QtWidgets,
@ -43,6 +43,11 @@ from PyQt6.QtCore import (
QSize, QSize,
QRect, QRect,
) )
from pyqtgraph import QtGui
# https://doc.qt.io/qt-6/highdpi.html#environment-variable-reference
os.environ['QT_USE_PHYSICAL_DPI'] = '1'

FWIW, i think we might want to add this setting by default since it seems (at least on sway/wayland) the “logical DPI” has very little value and is often plain deceiving since almost all compositor’s are going to pre-scale yet always report a 96..

at least this way (and try it urself via the updated script to verify) the “logical DPI” will be a rounded version, at least it seems, of the physical value; this at least discards ever using the “always 96 and not correct” default XD

FWIW, i think we might want to add this setting by default since it seems (at least on sway/wayland) the "logical DPI" has very little value and is often plain deceiving since almost all compositor's are going to pre-scale yet always report a 96.. at least this way (and try it urself via the updated script to verify) the "logical DPI" will be a rounded version, at least it seems, of the physical value; this at least discards ever using the "always 96 and not correct" default XD
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute # Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
# must be set before creating the application # must be set before creating the application
@ -58,13 +63,22 @@ if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
True, True,
) )
# NOTE, inherits `QGuiApplication`
# https://doc.qt.io/qt-6/qapplication.html
# https://doc.qt.io/qt-6/qguiapplication.html
app = QtWidgets.QApplication([]) app = QtWidgets.QApplication([])
#
# ^TODO? various global DPI settings?
# [ ] DPI rounding policy,

Not sure if this helps us more as well but figured i’d include it for further investigation.

Not sure if this helps us more as well but figured i'd include it for further investigation.
# - https://doc.qt.io/qt-6/qt.html#HighDpiScaleFactorRoundingPolicy-enum
# - https://doc.qt.io/qt-6/qguiapplication.html#setHighDpiScaleFactorRoundingPolicy
window = QtWidgets.QMainWindow() window = QtWidgets.QMainWindow()
main_widget = QtWidgets.QWidget() main_widget = QtWidgets.QWidget()
window.setCentralWidget(main_widget) window.setCentralWidget(main_widget)
window.show() window.show()
pxr: float = main_widget.devicePixelRatioF() _main_pxr: float = main_widget.devicePixelRatioF()
# explicitly get main widget and primary displays # explicitly get main widget and primary displays
current_screen: QtGui.QScreen = app.screenAt( current_screen: QtGui.QScreen = app.screenAt(
@ -77,7 +91,13 @@ for screen in app.screens():
name: str = screen.name() name: str = screen.name()
model: str = screen.model().rstrip() model: str = screen.model().rstrip()
size: QSize = screen.size() size: QSize = screen.size()
geo: QRect = screen.availableGeometry() geo: QRect = screen.geometry()
# device-pixel-ratio
# https://doc.qt.io/qt-6/highdpi.html
pxr: float = screen.devicePixelRatio()
unscaled_size: QSize = pxr * size

Note this should actually be per-screen and the correct abs resolution dimensions (in pxs obvi).

A further gotcha here on wayland is that only int values of pxr are able to be read.. which means anyone doing fancy float reso-scaling (like i was in my sway config) will get a wrong calculation for this..

I’m not exactly sure how to guard against this yet but at the least we can document that it’s unsupported for now, possibly a warning in the .configure_to_dpi() message; it would be best if we can actually detect that case but i found no immediately obvious cross-platform way other then something gemini recommended,

from screeninfo import get_monitors

print("Monitor Information:")
for i, m in enumerate(get_monitors()):
    print(f"Monitor {i}:")
    print(f"  Name: {m.name}")
    print(f"  Resolution (pixels): {m.width}x{m.height}")
    # Physical dimensions are provided in millimeters (mm)
    print(f"  Physical Size (mm): {m.width_mm}mm x {m.height_mm}mm")
    print(f"  Is Primary: {m.is_primary}")

main issue is that the screeninfo lib doesn’t natively support wayland except through xwayland.. so we need another wayland nodding approach as well.

a couple options on this front might be pywayland and pywlroots and i also was already sniffing at python-wayland for use in modden as an eventual gesture daemon approach (for mobile / touch screens that is). Anyway, this is more of a me only problem i’d imagine since i seem be one of the few using piker on wayland Bp


OH RIGHT XD

i forgot gemini also reco-ed tnkinter which is in the stdlib,

import tkinter as tk

def get_screen_resolution_tkinter():
    """
    Gets the primary screen resolution using Tkinter.
    Works on Windows, macOS, and Linux (including Wayland/X11).
    """
    root = tk.Tk()
    # Hide the main window
    root.withdraw()
    
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    
    # Destroy the Tkinter instance after getting the information
    root.destroy()
    
    return screen_width, screen_height

if __name__ == "__main__":
    width, height = get_screen_resolution_tkinter()
    print(f"Screen resolution: {width}x{height}")

but i don’t think we can get the “physical dimensions” (like in cm/inches) from this? needs some tinkering if you feel up for it @momo ;)

Note this should actually be per-screen and the correct abs resolution dimensions (in pxs obvi). A further gotcha here on `wayland` is that only `int` values of `pxr` are able to be read.. which means anyone doing fancy `float` reso-scaling (like i was in my `sway` config) will get a wrong calculation for this.. I'm not exactly sure how to guard against this yet but at the least we can document that it's unsupported for now, possibly a warning in the `.configure_to_dpi()` message; it would be best if we can actually detect that case but i found no immediately obvious cross-platform way other then something `gemini` recommended, ```python from screeninfo import get_monitors print("Monitor Information:") for i, m in enumerate(get_monitors()): print(f"Monitor {i}:") print(f" Name: {m.name}") print(f" Resolution (pixels): {m.width}x{m.height}") # Physical dimensions are provided in millimeters (mm) print(f" Physical Size (mm): {m.width_mm}mm x {m.height_mm}mm") print(f" Is Primary: {m.is_primary}") ``` main issue is that the `screeninfo` lib doesn't natively support `wayland` except through `xwayland`.. so we need another wayland nodding approach as well. a couple options on this front might be `pywayland` and `pywlroots` and i also was already sniffing at [python-wayland](https://python-wayland.org/) for use in `modden` as an eventual gesture daemon approach (for mobile / touch screens that is). Anyway, this is more of a *me only* problem i'd imagine since i seem be one of the few using `piker` on `wayland` Bp --- OH RIGHT XD i forgot `gemini` also reco-ed `tnkinter` which is in the stdlib, ```python import tkinter as tk def get_screen_resolution_tkinter(): """ Gets the primary screen resolution using Tkinter. Works on Windows, macOS, and Linux (including Wayland/X11). """ root = tk.Tk() # Hide the main window root.withdraw() screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() # Destroy the Tkinter instance after getting the information root.destroy() return screen_width, screen_height if __name__ == "__main__": width, height = get_screen_resolution_tkinter() print(f"Screen resolution: {width}x{height}") ``` but i don't think we can get the "physical dimensions" (like in cm/inches) from this? needs some tinkering if you feel up for it @momo ;)
phydpi: float = screen.physicalDotsPerInch() phydpi: float = screen.physicalDotsPerInch()
logdpi: float = screen.logicalDotsPerInch() logdpi: float = screen.logicalDotsPerInch()
is_primary: bool = screen is primary_screen is_primary: bool = screen is primary_screen
@ -88,11 +108,12 @@ for screen in app.screens():
f'|_primary: {is_primary}\n' f'|_primary: {is_primary}\n'
f' _current: {is_current}\n' f' _current: {is_current}\n'
f' _model: {model}\n' f' _model: {model}\n'
f' _screen size: {size}\n' f' _size: {size}\n'
f' _screen geometry: {geo}\n' f' _geometry: {geo}\n'
f' _devicePixelRationF(): {pxr}\n' f' _devicePixelRatio(): {pxr}\n'
f' _physical dpi: {phydpi}\n' f' _unscaled-size: {unscaled_size!r}\n'
f' _logical dpi: {logdpi}\n' f' _physical-dpi: {phydpi}\n'
f' _logical-dpi: {logdpi}\n'
) )
# app-wide font info # app-wide font info
@ -110,8 +131,8 @@ str_w: int = str_br.width()
print( print(
f'------ global font settings ------\n' f'------ global font settings ------\n'
f'font dpi: {fontdpi}\n' f'font dpi: {fontdpi!r}\n'
f'font height: {font_h}\n' f'font height: {font_h!r}\n'
f'string bounding rect: {str_br}\n' f'string bounding rect: {str_br!r}\n'
f'string width : {str_w}\n' f'string width : {str_w!r}\n'
) )