Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
adaf069
Add a ctypes wrapper for fontconfig.
freakboy3742 Apr 5, 2023
cd0fcb2
Add Changenote.
freakboy3742 Apr 5, 2023
f706ade
Improved error reporting for unknown fonts.
freakboy3742 Apr 6, 2023
d2afb95
Fonts core API to 100%. Also fixes some stray canvas tests dependent …
freakboy3742 Apr 6, 2023
2732de1
Add an extra test for registering font variants.
freakboy3742 Apr 6, 2023
dca074d
Removed measure as a public API on font.
freakboy3742 Apr 6, 2023
11777d3
Removed a now-redundant test.
freakboy3742 Apr 6, 2023
1395706
Add fonts to testbed
studioph Apr 25, 2023
da65018
Quote font family when creating CSS stylesheet
studioph Apr 25, 2023
bc2823d
Use dict to convert Pango enums to Toga
studioph Apr 25, 2023
c4778d7
Add widget_probe fixture to testbed conftest
studioph Apr 25, 2023
0d31ea0
Add font tests to testbed
studioph Apr 25, 2023
d20ea5b
Move font measure to canvas
studioph Apr 25, 2023
32b18e9
Add changenote
studioph Apr 25, 2023
2e27817
Fix app path in tests
studioph Apr 26, 2023
8451948
Use app fixture
studioph Apr 26, 2023
6f3325f
Merge branch 'main' into fontconfig
freakboy3742 Apr 26, 2023
0ae230e
Merge branch 'main' into fontconfig
freakboy3742 May 3, 2023
3cd10a8
Corrected a spelling error.
freakboy3742 May 3, 2023
2822635
Merge branch 'fontconfig' into gtk-font-tests
freakboy3742 May 3, 2023
67e17c1
Remove redundant platform font test.
freakboy3742 May 3, 2023
30e638b
Minor cleanup of GTK probe implementation.
freakboy3742 May 3, 2023
bf78924
Modify the not-a-font file to make the purpose explicit.
freakboy3742 May 3, 2023
d1ea0e7
Minor code format cleanup.
freakboy3742 May 3, 2023
93a5874
Small tweaks to tests.
freakboy3742 May 3, 2023
707a0b8
Tweaked the GTK font tests.
freakboy3742 May 3, 2023
1405606
Merge branch 'main' into gtk-font-tests
freakboy3742 May 3, 2023
c8134fc
Ensure webkit2 libraries are available for testing.
freakboy3742 May 3, 2023
fc9ada3
Update Winforms to get 100% font test coverage.
freakboy3742 May 3, 2023
3a814ad
Update docs to reflect support status and Winforms limitations.
freakboy3742 May 3, 2023
6572d97
Clarify comment around coverage case.
freakboy3742 May 14, 2023
9d644e4
Clarify description of default font size.
freakboy3742 May 14, 2023
ffc3424
Restructured code to avoid false KeyError hits.
freakboy3742 May 14, 2023
16c4930
Clarified comment about FontConfig singleton.
freakboy3742 May 14, 2023
510abc9
Check for additional failure modes when loading custom fonts.
freakboy3742 May 14, 2023
a58e376
Rework font tests to test all variants.
freakboy3742 May 15, 2023
a267763
Restructure winforms font loading to isolate keyerror failures.
freakboy3742 May 15, 2023
5fa3011
Moved all Winforms font handling into the font class to allow for cus…
freakboy3742 May 15, 2023
6fa4f6f
Made the winforms font test more complete.
freakboy3742 May 15, 2023
58b9039
Interpret OBLIQUE as ITALIC.
freakboy3742 May 17, 2023
a27be03
Correct typo.
freakboy3742 May 17, 2023
bc832e9
Merge branch 'main' into gtk-font-tests
freakboy3742 Jun 13, 2023
c4d8a57
Refactor app path into interface layer.
freakboy3742 Jun 14, 2023
95d6b3c
Merge branch 'main' into gtk-font-tests
freakboy3742 Jun 24, 2023
e53c7ea
Corrections to Winforms and testbed tests.
freakboy3742 Jun 25, 2023
34cc18d
Try a different approach to avoid path discrepancy issues.
freakboy3742 Jul 2, 2023
c6b63c1
Merge remote-tracking branch 'upstream/main' into gtk-font-tests
freakboy3742 Jul 2, 2023
f863568
Add 100% font coverage for Cocoa.
freakboy3742 Jul 2, 2023
0d32913
Restructure font probe into a standalone mixin.
freakboy3742 Jul 3, 2023
d246db4
100% coverage for iOS fonts.
freakboy3742 Jul 3, 2023
c5836d2
Android fonts working for all but custom fonts.
freakboy3742 Jul 3, 2023
dca6219
Correct GTK test for custom font.
freakboy3742 Jul 3, 2023
dbafa75
Add safety rail when using Dummy backend.
freakboy3742 Jul 3, 2023
424014d
Small tweak to box child creation to ensure custom fonts work on GTK.
freakboy3742 Jul 4, 2023
c58d20e
Merge branch 'main' into gtk-font-tests
freakboy3742 Jul 9, 2023
6281eb0
Merge branch 'main' into gtk-font-tests
freakboy3742 Jul 14, 2023
c67cb76
Merge branch 'main' into gtk-font-tests
freakboy3742 Jul 17, 2023
69fab4f
Merge branch 'main' into gtk-font-tests
freakboy3742 Jul 26, 2023
7e2c327
Merge branch 'main' into gtk-font-tests
freakboy3742 Jul 26, 2023
da83ddf
Merge branch 'main' into gtk-font-tests
freakboy3742 Aug 4, 2023
131503d
Correct spelling errors.
freakboy3742 Aug 4, 2023
0c07e4c
Merge branch 'main' into gtk-font-tests
freakboy3742 Aug 25, 2023
a978bc7
Merge branch 'main' into gtk-font-tests
freakboy3742 Sep 10, 2023
3531f9f
Bump PyGObject version to incorporate bugfix for PyGObject bug 119.
freakboy3742 Sep 11, 2023
d22539a
Clarify that Android Selection doesn't support font, color or alignment
mhsmith Sep 11, 2023
5821c65
Documentation cleanups
mhsmith Sep 12, 2023
86015f4
Complete transition to keyword-only arguments
mhsmith Sep 12, 2023
86bb969
Fix Android Table font tests, and add expected font sizes to probes
mhsmith Sep 12, 2023
026f957
Add an example of adding a label with a font after initial construction.
freakboy3742 Sep 12, 2023
3862545
100% coverage for Android fonts
mhsmith Sep 13, 2023
4b9c84a
Documentation fixes
mhsmith Sep 13, 2023
a0933e9
Revert the child creation order.
freakboy3742 Sep 13, 2023
e69d97e
Clarify an android requirements as a test requirement.
freakboy3742 Sep 13, 2023
dce0223
Reorganize docs for Font.
freakboy3742 Sep 13, 2023
7036ec4
Correct test case for child creation order.
freakboy3742 Sep 13, 2023
33931fa
Complete reversion of child creation order
mhsmith Sep 14, 2023
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
55 changes: 26 additions & 29 deletions android/src/toga_android/fonts.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import os
from pathlib import Path

import toga
from toga.fonts import (
_REGISTERED_FONT_CACHE,
BOLD,
CURSIVE,
FANTASY,
ITALIC,
MESSAGE,
MONOSPACE,
OBLIQUE,
SANS_SERIF,
SERIF,
SYSTEM,
SYSTEM_DEFAULT_FONT_SIZE,
SYSTEM_DEFAULT_FONTS,
)
from toga_android.libs.android.graphics import Typeface
from toga_android.libs.android.util import TypedValue
Expand Down Expand Up @@ -44,24 +46,28 @@ def apply(self, tv, default_size, default_typeface):
typeface = _FONT_CACHE[cache_key]
except KeyError:
typeface = None
font_key = self.interface.registered_font_key(
font_key = self.interface._registered_font_key(
self.interface.family,
weight=self.interface.weight,
style=self.interface.style,
variant=self.interface.variant,
)
if font_key in _REGISTERED_FONT_CACHE:
font_path = str(
toga.App.app.paths.app / _REGISTERED_FONT_CACHE[font_key]
)
if os.path.isfile(font_path):
try:
font_path = _REGISTERED_FONT_CACHE[font_key]
except KeyError:
# Not a pre-registered font
if self.interface.family not in SYSTEM_DEFAULT_FONTS:
print(
f"Unknown font '{self.interface}'; "
"using system font as a fallback"
)
else:
if Path(font_path).is_file():
typeface = Typeface.createFromFile(font_path)
# If the typeface cannot be created, following Exception is thrown:
# E/Minikin: addFont failed to create font, invalid request
# It does not kill the app, but there is currently no way to
# catch this Exception on Android
if typeface is Typeface.DEFAULT:
raise ValueError(f"Unable to load font file {font_path}")
else:
print(f"Registered font path {font_path!r} could not be found")
raise ValueError(f"Font file {font_path} could not be found")

if typeface is None:
if self.interface.family is SYSTEM:
Expand All @@ -70,6 +76,8 @@ def apply(self, tv, default_size, default_typeface):
# (600 or 700). To preserve this, we use the widget's original
# typeface as a starting point rather than Typeface.DEFAULT.
typeface = default_typeface
elif self.interface.family is MESSAGE:
typeface = Typeface.DEFAULT
elif self.interface.family is SERIF:
typeface = Typeface.SERIF
elif self.interface.family is SANS_SERIF:
Expand All @@ -87,25 +95,14 @@ def apply(self, tv, default_size, default_typeface):
typeface = Typeface.create(self.interface.family, Typeface.NORMAL)

native_style = typeface.getStyle()
if self.interface.weight is not None:
native_style = set_bits(
native_style, Typeface.BOLD, self.interface.weight == BOLD
)
if self.interface.style is not None:
native_style = set_bits(
native_style, Typeface.ITALIC, self.interface.style == ITALIC
)
if self.interface.weight == BOLD:
native_style |= Typeface.BOLD
if self.interface.style in {ITALIC, OBLIQUE}:
native_style |= Typeface.ITALIC

if native_style != typeface.getStyle():
typeface = Typeface.create(typeface, native_style)

_FONT_CACHE[cache_key] = typeface

tv.setTypeface(typeface)


def set_bits(input, mask, enable=True):
if enable:
output = input | mask
else:
output = input & ~mask
return output
7 changes: 2 additions & 5 deletions android/src/toga_android/widgets/selection.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from travertino.size import at_least

from ..libs.android import R__layout
from ..libs.android.view import Gravity, View__MeasureSpec
from ..libs.android.view import View__MeasureSpec
from ..libs.android.widget import ArrayAdapter, OnItemSelectedListener, Spinner
from .base import Widget, align
from .base import Widget


class TogaOnItemSelectedListener(OnItemSelectedListener):
Expand Down Expand Up @@ -89,6 +89,3 @@ def rehint(self):
)
self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth())
self.interface.intrinsic.height = self.native.getMeasuredHeight()

def set_alignment(self, value):
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
Comment on lines -93 to -94
Copy link
Member

@mhsmith mhsmith Sep 11, 2023

Choose a reason for hiding this comment

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

This had no effect because the gravity we actually need to change is that of the TextView within the R__layout.simple_spinner_item. This can be fixed later, along with color and font (#1758).

107 changes: 107 additions & 0 deletions android/tests_backend/fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from concurrent.futures import ThreadPoolExecutor

from fontTools.ttLib import TTFont
from java import jint
from java.lang import Integer, Long

from android.graphics import Typeface
from android.graphics.fonts import FontFamily
from android.util import TypedValue
from toga.fonts import (
BOLD,
ITALIC,
MESSAGE,
NORMAL,
OBLIQUE,
SMALL_CAPS,
SYSTEM,
SYSTEM_DEFAULT_FONT_SIZE,
)

SYSTEM_FONTS = {}
nativeGetFamily = new_FontFamily = None


def load_fontmap():
field = Typeface.getClass().getDeclaredField("sSystemFontMap")
field.setAccessible(True)
fontmap = field.get(None)

for name in fontmap.keySet().toArray():
typeface = fontmap.get(name)
SYSTEM_FONTS[typeface] = name
for native_style in [
Typeface.BOLD,
Typeface.ITALIC,
Typeface.BOLD | Typeface.ITALIC,
]:
SYSTEM_FONTS[Typeface.create(typeface, native_style)] = name


def reflect_font_methods():
global nativeGetFamily, new_FontFamily

# Bypass non-SDK interface restrictions by looking them up on a background thread
# with no Java stack frames (https://stackoverflow.com/a/61600526).
Copy link
Member

Choose a reason for hiding this comment

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

Based on the StackOverflow discussion, I'm utterly petrified about what will happen to this as Android releases new versions... but until then, it's a nice hack. (well, it's an utterly ugly hack, but it's a nice utterly ugly hack :-) )

with ThreadPoolExecutor() as executor:
nativeGetFamily = executor.submit(
Typeface.getClass().getDeclaredMethod,
"nativeGetFamily",
Long.TYPE,
Integer.TYPE,
).result()
nativeGetFamily.setAccessible(True)

new_FontFamily = executor.submit(
FontFamily.getClass().getConstructor, Long.TYPE
).result()


class FontMixin:
supports_custom_fonts = True

def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):
assert (BOLD if self.typeface.isBold() else NORMAL) == weight

if style == OBLIQUE:
print("Interpreting OBLIQUE font as ITALIC")
assert self.typeface.isItalic()
else:
assert (ITALIC if self.typeface.isItalic() else NORMAL) == style

if variant == SMALL_CAPS:
print("Ignoring SMALL CAPS font test")
else:
assert NORMAL == variant

def assert_font_size(self, expected):
if expected == SYSTEM_DEFAULT_FONT_SIZE:
expected = self.default_font_size
assert round(self.text_size) == round(
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
expected,
self.native.getResources().getDisplayMetrics(),
)
)

def assert_font_family(self, expected):
if not SYSTEM_FONTS:
load_fontmap()

if actual := SYSTEM_FONTS.get(self.typeface):
assert actual == {
SYSTEM: self.default_font_family,
MESSAGE: "sans-serif",
}.get(expected, expected)
else:
if not nativeGetFamily:
reflect_font_methods()
family_ptr = nativeGetFamily.invoke(
None, self.typeface.native_instance, jint(0)
)
family = new_FontFamily.newInstance(family_ptr)
assert family.getSize() == 1

font = TTFont(family.getFont(0).getFile().getPath())
assert font["name"].getDebugName(1) == expected
9 changes: 0 additions & 9 deletions android/tests_backend/probe.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import asyncio

from toga.fonts import SYSTEM


class BaseProbe:
def assert_font_family(self, expected):
actual = self.font.family
if expected == SYSTEM:
assert actual == "sans-serif"
else:
assert actual == expected

async def redraw(self, message=None, delay=None):
"""Request a redraw of the app, waiting until that redraw has completed."""
# If we're running slow, wait for a second
Expand Down
7 changes: 6 additions & 1 deletion android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from toga.colors import TRANSPARENT
from toga.style.pack import JUSTIFY, LEFT

from ..fonts import FontMixin
from ..probe import BaseProbe
from .properties import toga_color, toga_vertical_alignment

Expand All @@ -33,11 +34,15 @@ def onGlobalLayout(self):
self.event.set()


class SimpleProbe(BaseProbe):
class SimpleProbe(BaseProbe, FontMixin):
default_font_family = "sans-serif"
default_font_size = 14

def __init__(self, widget):
super().__init__()
self.app = widget.app
self.widget = widget
self.impl = widget._impl
self.native = widget._impl.native
self.layout_listener = LayoutListener()
self.native.getViewTreeObserver().addOnGlobalLayoutListener(
Expand Down
9 changes: 2 additions & 7 deletions android/tests_backend/widgets/button.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from java import jclass

from toga.colors import TRANSPARENT
from toga.fonts import SYSTEM

from .label import LabelProbe

Expand All @@ -10,12 +9,8 @@
class ButtonProbe(LabelProbe):
native_class = jclass("android.widget.Button")

def assert_font_family(self, expected):
actual = self.font.family
if expected == SYSTEM:
assert actual == "sans-serif-medium"
else:
assert actual == expected
# Heavier than sans-serif, but lighter than sans-serif bold
default_font_family = "sans-serif-medium"

@property
def background_color(self):
Expand Down
14 changes: 7 additions & 7 deletions android/tests_backend/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from android.os import Build

from .base import SimpleProbe
from .properties import toga_alignment, toga_color, toga_font
from .properties import toga_alignment, toga_color


class LabelProbe(SimpleProbe):
Expand All @@ -19,12 +19,12 @@ def text(self):
return str(self.native.getText())

@property
def font(self):
return toga_font(
self.native.getTypeface(),
self.native.getTextSize(),
self.native.getResources(),
)
def typeface(self):
return self.native.getTypeface()

@property
def text_size(self):
return self.native.getTextSize()

@property
def alignment(self):
Expand Down
10 changes: 1 addition & 9 deletions android/tests_backend/widgets/passwordinput.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
from toga.fonts import SYSTEM

from .textinput import TextInputProbe


class PasswordInputProbe(TextInputProbe):
# In password mode, the EditText defaults to monospace.
def assert_font_family(self, expected):
actual = self.font.family
if expected == SYSTEM:
assert actual == "monospace"
else:
assert actual == expected
default_font_family = "monospace"
Loading