Dynamic, auto-(re)sized/scaled UIs via PPI-driven-font-size calcs? #50

Open
opened 2025-12-19 19:26:32 +00:00 by goodboy · 2 comments

More or less a repost of https://github.com/pikers/piker/issues/159.

The jist of the idea is to calculate the ideal font size on any hardware display, dynamically, such that all other UI/X components (widgets, buttons, axes, labels) relative sizings are oriented around the idea that a word-on-a-line is the fundamental “visual object” to which all others can be relatively sized.

In other words given a hardware display with,

  • fixed physical dimensions
  • a fixed/set resolution
  • a scaling factor applied (by the DE, firmware, or other sw)

we should be able to,

  • select a target “words on a line should be X cm/inches tall”
  • given a chosen font and all the prior parameters, compute the closest font-size which will be (at most) X cm line-height physically on screen
  • dynamically re-calc this value for other displays and thus resize our Qt apps when moved across displays.

Current patch requests around this

  • #48 (and it’s soon-to-be merged child #49 )

Follow up research..

Qt docs,

various question-n-answer and follow-thru resources on “font-size”,

More or less a repost of https://github.com/pikers/piker/issues/159. The jist of the idea is to calculate the ideal font size on any hardware display, dynamically, such that all other UI/X components (widgets, buttons, axes, labels) relative sizings are oriented around the idea that a *word-on-a-line is the fundamental "visual object" to which all others can be relatively sized*. In other words given a hardware display with, - fixed physical dimensions - a fixed/set resolution - a scaling factor applied (by the DE, firmware, or other sw) we should be able to, - select a target "words on a line should be X cm/inches tall" - given a chosen font and all the prior parameters, compute the closest *font-size* which will be (at most) `X cm line-height` physically on screen - dynamically re-calc this value for other displays and thus resize our Qt apps when moved across displays. --- #### Current patch requests around this - #48 (and it's soon-to-be merged child #49 ) --- #### Follow up research.. `Qt` docs, - https://forum.qt.io/topic/52296/detect-changing-devicepixelratio - https://doc.qt.io/qt-6/highdpi.html various question-n-answer and follow-thru resources on "font-size", - https://stackoverflow.com/questions/42026239/what-does-font-size-really-correspond-to - https://stackoverflow.com/questions/55978130/can-specific-text-character-change-the-line-height - https://graphicdesign.stackexchange.com/questions/4035/what-does-the-size-of-the-font-translate-to-exactly - https://www.quora.com/How-can-you-calculate-the-pixels-per-inch-PPI-of-a-screen-without-measuring-it-directly * the *big question* which seems to have very non-straight-forward answer.. XD - https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align#lets-talk-about-font-size-first * a **just wow** deep-dive on how it all works in `CSS`; the amount of convoluted nonsense in this, wayyy surpassed my intuition on how annoying it would be to grok * here are some further embedded resources on various concepts from ^, - http://designwithfontforge.com/en-US/The_EM_Square.html - https://fontforge.org/en-US/ - https://www.thomasphinney.com/2011/03/point-size/
Poster
Owner

Re how to compute PPI (in this example on a wayland DE) using the formula from the quora answer linked above,

https://www.quora.com/How-can-you-calculate-the-pixels-per-inch-PPI-of-a-screen-without-measuring-it-directly

Obtain two values:

    - Horizontal pixel count (width in pixels), W.
    - Vertical pixel count (height in pixels), H.
    - Diagonal screen size in inches, D (commonly specified by manufacturers, e.g., 13.3", 6.7").

Compute the pixel diagonal:
    PixelDiagonal = sqrt(W^2 + H^2)
    
Compute PPI:
    PPI = PixelDiagonal / D

First get the display’s reported info,

(modden)  >>> wlr-randr --json | jq
[
  {
    "name": "eDP-1",
    "description": "BOE NE135A1M-NY1 (eDP-1)",
    "make": "BOE",
    "model": "NE135A1M-NY1",
    "serial": null,
    "physical_size": {
      "width": 290,
      "height": 190
    },
    "enabled": true,
    "modes": [
      {
        "width": 2880,
        "height": 1920,
        "refresh": 120.000000,
        "preferred": true,
        "current": false
      },
      {
        "width": 2880,
        "height": 1920,
        "refresh": 60.000999,
        "preferred": true,
        "current": true
      },
      {
        "width": 1920,
        "height": 1200,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1920,
        "height": 1080,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1600,
        "height": 1200,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1680,
        "height": 1050,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1280,
        "height": 1024,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1440,
        "height": 900,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1280,
        "height": 800,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1280,
        "height": 720,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 1024,
        "height": 768,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 800,
        "height": 600,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      },
      {
        "width": 640,
        "height": 480,
        "refresh": 120.000000,
        "preferred": false,
        "current": false
      }
    ],
    "position": {
      "x": 0,
      "y": 0
    },
    "transform": "normal",
    "scale": 2.000000,
    "adaptive_sync": false
  }
]

Using xonsh i can pre-process in python even nicer,

(modden)  >>> import json
...:...:...:. # XXX, xonsh part using "subprocess mode"
...:...:...:. json.loads($(wlr-randr --json))
[{'name': 'eDP-1',
  'description': 'BOE NE135A1M-NY1 (eDP-1)',
  'make': 'BOE',
  'model': 'NE135A1M-NY1',
  'serial': None,
  'physical_size': {'width': 290, 'height': 190},
  'enabled': True,
  'modes': [{'width': 2880,
    'height': 1920,
    'refresh': 120.0,
    'preferred': True,
    'current': False},
   {'width': 2880,
    'height': 1920,
    'refresh': 60.000999,
    'preferred': True,
    'current': True},
   {'width': 1920,
    'height': 1200,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1920,
    'height': 1080,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1600,
    'height': 1200,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1680,
    'height': 1050,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1280,
    'height': 1024,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1440,
    'height': 900,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1280,
    'height': 800,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1280,
    'height': 720,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 1024,
    'height': 768,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 800,
    'height': 600,
    'refresh': 120.0,
    'preferred': False,
    'current': False},
   {'width': 640,
    'height': 480,
    'refresh': 120.0,
    'preferred': False,
    'current': False}],
  'position': {'x': 0, 'y': 0},
  'transform': 'normal',
  'scale': 2.0,
  'adaptive_sync': False}]

now let’s take the formula from the quora answer and see if we can compute the PPI reported by our snippets/ script,


# TECHINCALLY this is `.xsh` (aka `xonsh` code)!
import math
import json

# XXX, xonsh code 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"info for goodboy's fw13 lappy\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'diag_inches: {diag_inches!r}\n'
    f'diag_pxs: {diag_pxs!r}\n'
    f'(DE reported) scale: {scale!r}\n'
    f'unscaled PPI: {unscaled_ppi!r}\n'
    f'scaled PPI: {unscaled_ppi/scale!r}\n'
)

# this gives the console output,

info for goodboy's fw13 lappy
w_cm: 290
h_cm: 190
w_px: 2880
h_cm: 1920
diag_inches: 13.649555766424974
diag_pxs: 3461.3292244454296
(DE reported) scale: 2.0
unscaled PPI: 253.58548539429896
scaled PPI: 126.79274269714948

now let’s compare that to our snippets/ script,

(piker)  >>>
...:...:...: os.environ['QT_USE_PHYSICAL_DPI'] = '1'
...:...:...: python -m snippets.qt_screen_info
------ screen name: eDP-1 ------
|_primary: True
 _current: True
 _model: NE135A1M-NY1
 _size: PyQt6.QtCore.QSize(1089, 726)
 _geometry: PyQt6.QtCore.QRect(0, 0, 1089, 726)
 _devicePixelRatio(): 2.6458333333333335
 _unscaled-size: PyQt6.QtCore.QSize(2881, 1921)
 _physical-dpi: 96.21805807622503
 _logical-dpi: 96.0

------ global font settings ------
font dpi: 96.0
font height: 7
string bounding rect: PyQt6.QtCore.QRect(0, -6, 20, 7)
string width : 20


(piker)  >>>
...:...:...: os.environ['QT_USE_PHYSICAL_DPI'] = '0'
...:...:...: python -m snippets.qt_screen_info
------ screen name: eDP-1 ------
|_primary: True
 _current: True
 _model: NE135A1M-NY1
 _size: PyQt6.QtCore.QSize(1440, 960)
 _geometry: PyQt6.QtCore.QRect(0, 0, 1440, 960)
 _devicePixelRatio(): 2.0
 _unscaled-size: PyQt6.QtCore.QSize(2880, 1920)
 _physical-dpi: 127.23049001814883
 _logical-dpi: 96.0

------ global font settings ------
font dpi: 96.0
font height: 7
string bounding rect: PyQt6.QtCore.QRect(0, -6, 20, 7)
string width : 20

… so interestingly enough, we can see how this QT_USE_PHYSICAL_DPI affects things lol,

  • turned off we get the same as our scaled_ppi = unscaled_ppi/scale = 127.23049001814883
  • with it turned on we get the literally out of nowhere and very INCORRECT _physical-dpi: 96.21805807622503
Re *how to compute PPI* (in this example on a wayland DE) using the formula from the quora answer linked above, https://www.quora.com/How-can-you-calculate-the-pixels-per-inch-PPI-of-a-screen-without-measuring-it-directly ``` Obtain two values: - Horizontal pixel count (width in pixels), W. - Vertical pixel count (height in pixels), H. - Diagonal screen size in inches, D (commonly specified by manufacturers, e.g., 13.3", 6.7"). Compute the pixel diagonal: PixelDiagonal = sqrt(W^2 + H^2) Compute PPI: PPI = PixelDiagonal / D ``` First get the display's reported info, ```python (modden) >>> wlr-randr --json | jq [ { "name": "eDP-1", "description": "BOE NE135A1M-NY1 (eDP-1)", "make": "BOE", "model": "NE135A1M-NY1", "serial": null, "physical_size": { "width": 290, "height": 190 }, "enabled": true, "modes": [ { "width": 2880, "height": 1920, "refresh": 120.000000, "preferred": true, "current": false }, { "width": 2880, "height": 1920, "refresh": 60.000999, "preferred": true, "current": true }, { "width": 1920, "height": 1200, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1920, "height": 1080, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1600, "height": 1200, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1680, "height": 1050, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1280, "height": 1024, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1440, "height": 900, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1280, "height": 800, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1280, "height": 720, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 1024, "height": 768, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 800, "height": 600, "refresh": 120.000000, "preferred": false, "current": false }, { "width": 640, "height": 480, "refresh": 120.000000, "preferred": false, "current": false } ], "position": { "x": 0, "y": 0 }, "transform": "normal", "scale": 2.000000, "adaptive_sync": false } ] ``` Using `xonsh` i can pre-process in python even nicer, ```python (modden) >>> import json ...:...:...:. # XXX, xonsh part using "subprocess mode" ...:...:...:. json.loads($(wlr-randr --json)) [{'name': 'eDP-1', 'description': 'BOE NE135A1M-NY1 (eDP-1)', 'make': 'BOE', 'model': 'NE135A1M-NY1', 'serial': None, 'physical_size': {'width': 290, 'height': 190}, 'enabled': True, 'modes': [{'width': 2880, 'height': 1920, 'refresh': 120.0, 'preferred': True, 'current': False}, {'width': 2880, 'height': 1920, 'refresh': 60.000999, 'preferred': True, 'current': True}, {'width': 1920, 'height': 1200, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1920, 'height': 1080, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1600, 'height': 1200, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1680, 'height': 1050, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1280, 'height': 1024, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1440, 'height': 900, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1280, 'height': 800, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1280, 'height': 720, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 1024, 'height': 768, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 800, 'height': 600, 'refresh': 120.0, 'preferred': False, 'current': False}, {'width': 640, 'height': 480, 'refresh': 120.0, 'preferred': False, 'current': False}], 'position': {'x': 0, 'y': 0}, 'transform': 'normal', 'scale': 2.0, 'adaptive_sync': False}] ``` now let's take the formula from the quora answer and see if we can compute the PPI reported by our `snippets/` script, ```python # TECHINCALLY this is `.xsh` (aka `xonsh` code)! import math import json # XXX, xonsh code 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"info for goodboy's fw13 lappy\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'diag_inches: {diag_inches!r}\n' f'diag_pxs: {diag_pxs!r}\n' f'(DE reported) scale: {scale!r}\n' f'unscaled PPI: {unscaled_ppi!r}\n' f'scaled PPI: {unscaled_ppi/scale!r}\n' ) # this gives the console output, info for goodboy's fw13 lappy w_cm: 290 h_cm: 190 w_px: 2880 h_cm: 1920 diag_inches: 13.649555766424974 diag_pxs: 3461.3292244454296 (DE reported) scale: 2.0 unscaled PPI: 253.58548539429896 scaled PPI: 126.79274269714948 ``` now let's compare that to our `snippets/` script, ```python (piker) >>> ...:...:...: os.environ['QT_USE_PHYSICAL_DPI'] = '1' ...:...:...: python -m snippets.qt_screen_info ------ screen name: eDP-1 ------ |_primary: True _current: True _model: NE135A1M-NY1 _size: PyQt6.QtCore.QSize(1089, 726) _geometry: PyQt6.QtCore.QRect(0, 0, 1089, 726) _devicePixelRatio(): 2.6458333333333335 _unscaled-size: PyQt6.QtCore.QSize(2881, 1921) _physical-dpi: 96.21805807622503 _logical-dpi: 96.0 ------ global font settings ------ font dpi: 96.0 font height: 7 string bounding rect: PyQt6.QtCore.QRect(0, -6, 20, 7) string width : 20 (piker) >>> ...:...:...: os.environ['QT_USE_PHYSICAL_DPI'] = '0' ...:...:...: python -m snippets.qt_screen_info ------ screen name: eDP-1 ------ |_primary: True _current: True _model: NE135A1M-NY1 _size: PyQt6.QtCore.QSize(1440, 960) _geometry: PyQt6.QtCore.QRect(0, 0, 1440, 960) _devicePixelRatio(): 2.0 _unscaled-size: PyQt6.QtCore.QSize(2880, 1920) _physical-dpi: 127.23049001814883 _logical-dpi: 96.0 ------ global font settings ------ font dpi: 96.0 font height: 7 string bounding rect: PyQt6.QtCore.QRect(0, -6, 20, 7) string width : 20 ``` ... so interestingly enough, we can see how this ` QT_USE_PHYSICAL_DPI` affects things lol, - turned off we get the same as our `scaled_ppi = unscaled_ppi/scale = 127.23049001814883` - with it **turned on** we get the literally out of nowhere and very **INCORRECT** `_physical-dpi: 96.21805807622503`
Poster
Owner

FYI pushed my .xsh script to #49,

e63cffaf53

FYI pushed my `.xsh` script to #49, e63cffaf532d09d51469e38fffc60ef0db3a0e35
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: pikers/piker#50
There is no content yet.