Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CSS font size keywords #3242

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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 @@ -96,6 +99,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
if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
if default is None:
typed_array = context.obtainStyledAttributes(
Expand All @@ -104,12 +108,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
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.
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 @@ -93,6 +96,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]
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: 1 addition & 1 deletion core/src/toga/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __str__(self) -> str:
size = (
"default size"
if self.size == SYSTEM_DEFAULT_FONT_SIZE
else f"{self.size}pt"
else f"{self.size}" if isinstance(self.size, str) else f"{self.size}pt"
)
weight = f" {self.weight}" if self.weight != NORMAL else ""
variant = f" {self.variant}" if self.variant != 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 @@ -7,6 +7,7 @@
from travertino.colors import rgb, hsl

from travertino.constants import ( # noqa: F401
ABSOLUTE_FONT_SIZES,
BOLD,
BOTTOM,
CENTER,
Expand Down Expand Up @@ -107,7 +108,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,
)

@classmethod
def _debug(cls, *args: str) -> None: # pragma: no cover
Expand Down Expand Up @@ -930,7 +935,10 @@ def __css__(self) -> str:
else:
css.append(f"font-family: {self.font_family};")
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 @@ -449,6 +449,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 @@ 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 @@ -287,11 +287,16 @@ The weight of the font to be used.
``font_size``
-------------

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

**Initial value:** System default

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 @@ -78,8 +82,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 @@ -92,6 +95,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 @@ -53,7 +56,11 @@ async def test_use_system_font_fallback(
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