Skip to content
Open
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
11 changes: 9 additions & 2 deletions android/src/toga_android/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from android.graphics import Typeface
from android.util import TypedValue
from org.beeware.android import MainActivity
from travertino.constants import (
FONT_SIZE_SCALE,
)

from toga.fonts import (
_REGISTERED_FONT_CACHE,
Expand Down Expand Up @@ -98,6 +101,7 @@ def typeface(self, *, default=Typeface.DEFAULT):
def size(self, *, default=None):
"""Return the font size in physical pixels."""
context = MainActivity.singletonThis
base_size = 14
Copy link
Member

@mhsmith mhsmith Apr 14, 2025

Choose a reason for hiding this comment

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

This size will need to be scaled in a similar way to the TypedValue.applyDimension code below. Otherwise, the text will be too small on high DPI devices (which is virtually all of them). For example, if I edit examples/font_size to use the font size keywords:

Screenshot_20250414_215333

As mentioned at #1814 (comment), on Android, as far as I know, "medium" should be the same as the default for every widget except the TextInput, where the default should be larger.

Please make a similar visual check on as many other platforms as you can, and let me know which ones you've checked. Windows in high DPI mode is the most likely one to have a problem.

if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
if default is None:
typed_array = context.obtainStyledAttributes(
Expand All @@ -106,12 +110,15 @@ def size(self, *, default=None):
default = typed_array.getDimension(0, 0)
typed_array.recycle()
return default

elif isinstance(self.interface.size, str):
default = base_size * FONT_SIZE_SCALE[self.interface.size]
return default
Comment on lines +114 to +115
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
default = base_size * FONT_SIZE_SCALE[self.interface.size]
return default
return base_size * FONT_SIZE_SCALE[self.interface.size]

else:
# Using SP means we follow the standard proportion between CSS pixels and
# points by default, but respect the system text scaling setting.
return TypedValue.applyDimension(
default = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
self.interface.size * (96 / 72),
context.getResources().getDisplayMetrics(),
)
return default
17 changes: 13 additions & 4 deletions android/tests_backend/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from fontTools.ttLib import TTFont
from java import jint
from java.lang import Integer, Long
from travertino.constants import (
FONT_SIZE_SCALE,
)

from toga.fonts import (
BOLD,
Expand Down Expand Up @@ -77,14 +80,20 @@ def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):

def assert_font_size(self, expected):
if expected == SYSTEM_DEFAULT_FONT_SIZE:
expected = self.default_font_size * (72 / 96)
assert round(self.text_size) == round(
TypedValue.applyDimension(
expected = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
self.default_font_size,
self.native.getResources().getDisplayMetrics(),
)
elif isinstance(expected, str):
expected = self.default_font_size * FONT_SIZE_SCALE[expected]
else:
expected = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
expected * (96 / 72),
self.native.getResources().getDisplayMetrics(),
)
)
assert round(self.text_size) == round(expected)

def assert_font_family(self, expected):
if not SYSTEM_FONTS:
Expand Down
1 change: 1 addition & 0 deletions changes/1814.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for absolute CSS font size keywords in Android, Cocoa, GTK, iOS, and Windows.
Copy link
Member

Choose a reason for hiding this comment

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

misc change notes are hidden in the release notes. This is a publicly-visible feature, so the file should be renamed to 1814.feature.rst.

6 changes: 6 additions & 0 deletions cocoa/src/toga_cocoa/fonts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from pathlib import Path

from fontTools.ttLib import TTFont
from travertino.constants import (
FONT_SIZE_SCALE,
)

from toga.fonts import (
_REGISTERED_FONT_CACHE,
Expand Down Expand Up @@ -88,6 +91,9 @@ def __init__(self, interface):

if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
font_size = NSFont.systemFontSize
elif isinstance(self.interface.size, str):
base_size = NSFont.systemFontSize
font_size = base_size * FONT_SIZE_SCALE[self.interface.size]
Comment on lines +95 to +96
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
base_size = NSFont.systemFontSize
font_size = base_size * FONT_SIZE_SCALE[self.interface.size]
font_size = NSFont.systemFontSize * FONT_SIZE_SCALE[self.interface.size]

else:
# A "point" in Apple APIs is equivalent to a CSS pixel, but the Toga
# public API works in CSS points, which are slightly larger
Expand Down
7 changes: 7 additions & 0 deletions cocoa/tests_backend/fonts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from travertino.constants import (
FONT_SIZE_SCALE,
)

from toga.fonts import (
BOLD,
CURSIVE,
Expand Down Expand Up @@ -48,6 +52,9 @@ def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):
def assert_font_size(self, expected):
if expected == SYSTEM_DEFAULT_FONT_SIZE:
assert self.font.pointSize == 13
elif isinstance(expected, str):
expected_size = 13 * FONT_SIZE_SCALE[expected]
assert abs(self.font.pointSize - expected_size) < 0.01
else:
assert self.font.pointSize == expected * 96 / 72

Expand Down
2 changes: 2 additions & 0 deletions core/src/toga/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def __str__(self) -> str:
size = (
"default size"
if self.size == SYSTEM_DEFAULT_FONT_SIZE
else f"{self.size}"
if isinstance(self.size, str)
else f"{self.size}pt"
)
weight = f" {self.weight}" if self.weight != NORMAL else ""
Expand Down
12 changes: 10 additions & 2 deletions core/src/toga/style/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from travertino.colors import hsl, rgb

from travertino.constants import ( # noqa: F401
ABSOLUTE_FONT_SIZES,
BOLD,
BOTTOM,
CENTER,
Expand Down Expand Up @@ -233,7 +234,11 @@ class IntrinsicSize(BaseIntrinsicSize):
font_style: str = validated_property(*FONT_STYLES, initial=NORMAL)
font_variant: str = validated_property(*FONT_VARIANTS, initial=NORMAL)
font_weight: str = validated_property(*FONT_WEIGHTS, initial=NORMAL)
font_size: int = validated_property(integer=True, initial=SYSTEM_DEFAULT_FONT_SIZE)
font_size: int | str = validated_property(
*ABSOLUTE_FONT_SIZES,
integer=True,
initial=SYSTEM_DEFAULT_FONT_SIZE,
)

######################################################################
# Directional aliases
Expand Down Expand Up @@ -978,7 +983,10 @@ def __css__(self) -> str:
]
css.append(f"font-family: {', '.join(families)};")
if self.font_size != SYSTEM_DEFAULT_FONT_SIZE:
css.append(f"font-size: {self.font_size}pt;")
if isinstance(self.font_size, str):
css.append(f"font-size: {self.font_size};")
else:
css.append(f"font-size: {self.font_size}pt;")
if self.font_weight != NORMAL:
css.append(f"font-weight: {self.font_weight};")
if self.font_style != NORMAL:
Expand Down
20 changes: 20 additions & 0 deletions core/tests/style/pack/test_css.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,26 @@
"flex-direction: row; flex: 0.0 0 auto; font-size: 42pt;",
id="font-size",
),
pytest.param(
Pack(font_size="small"),
"flex-direction: row; flex: 0.0 0 auto; font-size: small;",
id="font-size-small",
),
pytest.param(
Pack(font_size="xx-small"),
"flex-direction: row; flex: 0.0 0 auto; font-size: xx-small;",
id="font-size-xx-small",
),
pytest.param(
Pack(font_size="x-large"),
"flex-direction: row; flex: 0.0 0 auto; font-size: x-large;",
id="font-size-x-large",
),
pytest.param(
Pack(font_size="large"),
"flex-direction: row; flex: 0.0 0 auto; font-size: large;",
id="font-size-large",
),
pytest.param(
Pack(font_size=SYSTEM_DEFAULT_FONT_SIZE),
"flex-direction: row; flex: 0.0 0 auto;",
Expand Down
63 changes: 63 additions & 0 deletions core/tests/test_fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,69 @@ async def app():
NORMAL,
"system default size",
),
# Custom font, small size
(
"Custom Font",
"small",
NORMAL,
NORMAL,
NORMAL,
"Custom Font small",
),
# System font, medium size
(
SYSTEM,
"medium",
NORMAL,
NORMAL,
NORMAL,
"system medium",
),
# System font, large size
(
SYSTEM,
"large",
NORMAL,
NORMAL,
NORMAL,
"system large",
),
# System font, x-small size
(
SYSTEM,
"x-small",
NORMAL,
NORMAL,
NORMAL,
"system x-small",
),
# System font, xx-small size
(
SYSTEM,
"xx-small",
NORMAL,
NORMAL,
NORMAL,
"system xx-small",
),
# System font, xx-large size
(
SYSTEM,
"xx-large",
NORMAL,
NORMAL,
NORMAL,
"system xx-large",
),
# System font, x-large size
(
SYSTEM,
"x-large",
NORMAL,
NORMAL,
NORMAL,
"system x-large",
),
],
)
def test_builtin_font(family, size, weight, style, variant, as_str):
Expand Down
9 changes: 7 additions & 2 deletions docs/reference/style/pack.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,17 @@ The weight of the font to be used.
``font_size``
-------------

**Value:** an integer
**Values:**
- ``<integer>`` (in :ref:`CSS points <css-units>`)
- Absolute keywords: ``xx-small`` | ``x-small`` | ``small`` | ``medium`` | ``large`` | ``x-large`` | ``xx-large``

**Initial value:** ``-1``; will use the system default size. This is also stored as a
constant named ``SYSTEM_DEFAULT_FONT_SIZE``.

The size of the font to be used, in :ref:`CSS points <css-units>`.
The size of the font to be used. Can be specified in the following ways:

* An integer value in :ref:`CSS points <css-units>`
* An absolute size keyword, which sets the size relative to the system's base font size

The relationship between Pack and CSS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
12 changes: 10 additions & 2 deletions gtk/src/toga_gtk/fonts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from pathlib import Path
from warnings import warn

from travertino.constants import (
ABSOLUTE_FONT_SIZES,
)

from toga.fonts import (
_REGISTERED_FONT_CACHE,
BOLD,
Expand Down Expand Up @@ -76,8 +80,12 @@ def __init__(self, interface):

font.set_family(family)

# If this is a non-default font size, set the font size
if self.interface.size != SYSTEM_DEFAULT_FONT_SIZE:
# Default font as well as values in absolute and relative font
# size are handled by Pango. Otherwise set font size manually.
if (
self.interface.size != SYSTEM_DEFAULT_FONT_SIZE
and self.interface.size not in ABSOLUTE_FONT_SIZES
):
font.set_size(self.interface.size * Pango.SCALE)

# Set font style
Expand Down
9 changes: 8 additions & 1 deletion gtk/src/toga_gtk/libs/styles.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from travertino.constants import (
ABSOLUTE_FONT_SIZES,
)

from toga.colors import TRANSPARENT
from toga.fonts import SYSTEM_DEFAULT_FONT_SIZE

Expand Down Expand Up @@ -61,7 +65,10 @@ def get_font_css(value):
"font-family": f"{value.family!r}",
}

if value.size != SYSTEM_DEFAULT_FONT_SIZE:
# If value is an absolute or relative keyword, use those to set size instead
if value.size in ABSOLUTE_FONT_SIZES:
style["font-size"] = f"{value.size}"
elif value.size != SYSTEM_DEFAULT_FONT_SIZE:
style["font-size"] = f"{value.size}pt"

return style
8 changes: 8 additions & 0 deletions gtk/tests_backend/fonts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from travertino.constants import (
ABSOLUTE_FONT_SIZES,
FONT_SIZE_SCALE,
)

from toga.fonts import (
BOLD,
ITALIC,
Expand Down Expand Up @@ -29,6 +34,9 @@ def assert_font_size(self, expected):
assert expected == SYSTEM_DEFAULT_FONT_SIZE
elif expected == SYSTEM_DEFAULT_FONT_SIZE:
assert 8 < int(self.font.get_size() / Pango.SCALE) < 18
elif expected in ABSOLUTE_FONT_SIZES:
scale = FONT_SIZE_SCALE[expected]
assert 8 * scale < int(self.font.get_size() / Pango.SCALE) < 18 * scale
else:
assert int(self.font.get_size() / Pango.SCALE) == expected

Expand Down
5 changes: 5 additions & 0 deletions iOS/src/toga_iOS/fonts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from pathlib import Path

from fontTools.ttLib import TTFont
from travertino.constants import (
FONT_SIZE_SCALE,
)

from toga.fonts import (
_REGISTERED_FONT_CACHE,
Expand Down Expand Up @@ -88,6 +91,8 @@ def __init__(self, interface):

if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
size = UIFont.labelFontSize
elif isinstance(self.interface.size, str):
size = UIFont.labelFontSize * FONT_SIZE_SCALE[self.interface.size]
else:
# A "point" in Apple APIs is equivalent to a CSS pixel, but the Toga
# public API works in CSS points, which are slightly larger
Expand Down
7 changes: 7 additions & 0 deletions iOS/tests_backend/fonts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from travertino.constants import (
FONT_SIZE_SCALE,
)

from toga.fonts import (
BOLD,
CURSIVE,
Expand Down Expand Up @@ -51,6 +55,9 @@ def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):
def assert_font_size(self, expected):
if expected == SYSTEM_DEFAULT_FONT_SIZE:
assert self.font.pointSize == 17
elif isinstance(expected, str):
expected_size = 17 * FONT_SIZE_SCALE[expected]
assert abs(self.font.pointSize - expected_size) < 0.01
else:
assert self.font.pointSize == expected * 96 / 72

Expand Down
9 changes: 8 additions & 1 deletion testbed/tests/test_fonts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from importlib import import_module

import pytest
from travertino.constants import (
ABSOLUTE_FONT_SIZES,
)

import toga
from toga.fonts import (
Expand Down Expand Up @@ -88,7 +91,11 @@ async def test_use_first_valid_font(
async def test_font_options(widget: toga.Label, font_probe):
"""Every combination of weight, style and variant can be used on a font."""
for font_family in SYSTEM_DEFAULT_FONTS:
for font_size in [20, SYSTEM_DEFAULT_FONT_SIZE]:
for font_size in [
20,
SYSTEM_DEFAULT_FONT_SIZE,
*ABSOLUTE_FONT_SIZES,
]:
for font_weight in FONT_WEIGHTS:
for font_style in FONT_STYLES:
for font_variant in FONT_VARIANTS:
Expand Down
Loading
Loading