Skip to content

Commit

Permalink
Merge pull request #1399 from freakboy3742/cocoa-fonts
Browse files Browse the repository at this point in the history
Initial implementation of Cocoa and iOS font loading.
  • Loading branch information
mhsmith committed Oct 19, 2023
2 parents 18137ad + e67de2e commit 7a46d41
Show file tree
Hide file tree
Showing 17 changed files with 198 additions and 183 deletions.
1 change: 1 addition & 0 deletions android/tests_backend/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def reflect_font_methods():

class FontMixin:
supports_custom_fonts = True
supports_custom_variable_fonts = True

def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):
assert (BOLD if self.typeface.isBold() else NORMAL) == weight
Expand Down
2 changes: 1 addition & 1 deletion changes/1837.feature.rst
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Support for custom font loading was added to the GTK backend.
Support for custom font loading was added to the GTK, Cocoa and iOS backends.
1 change: 1 addition & 0 deletions cocoa/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
setup(
version=version,
install_requires=[
"fonttools >= 4.42.1, < 5.0.0",
"rubicon-objc >= 0.4.5rc1, < 0.5.0",
f"toga-core == {version}",
],
Expand Down
95 changes: 58 additions & 37 deletions cocoa/src/toga_cocoa/fonts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from pathlib import Path

from fontTools.ttLib import TTFont

from toga.fonts import (
_REGISTERED_FONT_CACHE,
BOLD,
Expand All @@ -14,46 +16,80 @@
SMALL_CAPS,
SYSTEM,
SYSTEM_DEFAULT_FONT_SIZE,
SYSTEM_DEFAULT_FONTS,
)
from toga_cocoa.libs import (
NSURL,
NSFont,
NSFontManager,
NSFontMask,
)
from toga_cocoa.libs.core_text import core_text, kCTFontManagerScopeProcess

_FONT_CACHE = {}
_CUSTOM_FONT_NAMES = {}


class Font:
def __init__(self, interface):
self.interface = interface
try:
font = _FONT_CACHE[self.interface]
attributed_font = _FONT_CACHE[self.interface]
except KeyError:
font_family = self.interface.family
font_key = self.interface._registered_font_key(
self.interface.family,
family=font_family,
weight=self.interface.weight,
style=self.interface.style,
variant=self.interface.variant,
)

try:
font_path = _REGISTERED_FONT_CACHE[font_key]
# Built in fonts have known names; no need to interrogate a file.
custom_font_name = {
SYSTEM: None, # No font name required
MESSAGE: None, # No font name required
SERIF: "Times-Roman",
SANS_SERIF: "Helvetica",
CURSIVE: "Apple Chancery",
FANTASY: "Papyrus",
MONOSPACE: "Courier New",
}[font_family]
except KeyError:
# Not a pre-registered font
if self.interface.family not in SYSTEM_DEFAULT_FONTS:
try:
font_path = _REGISTERED_FONT_CACHE[font_key]
except KeyError:
# The requested font has not been registered
print(
f"Unknown font '{self.interface}'; "
"using system font as a fallback"
)
else:
if Path(font_path).is_file():
# TODO: Load font file
self.interface.factory.not_implemented("Custom font loading")
# if corrupted font file:
# raise ValueError(f"Unable to load font file {font_path}")
font_family = SYSTEM
custom_font_name = None
else:
raise ValueError(f"Font file {font_path} could not be found")
# We have a path for a font file.
try:
# A font *file* an only be registered once under Cocoa.
custom_font_name = _CUSTOM_FONT_NAMES[font_path]
except KeyError:
if Path(font_path).is_file():
font_url = NSURL.fileURLWithPath(font_path)
success = core_text.CTFontManagerRegisterFontsForURL(
font_url, kCTFontManagerScopeProcess, None
)
if success:
ttfont = TTFont(font_path)
custom_font_name = ttfont["name"].getBestFullName()
# Preserve the Postscript font name contained in the
# font file.
_CUSTOM_FONT_NAMES[font_path] = custom_font_name
else:
raise ValueError(
f"Unable to load font file {font_path}"
)
else:
raise ValueError(
f"Font file {font_path} could not be found"
)

if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
font_size = NSFont.systemFontSize
Expand All @@ -63,28 +99,13 @@ def __init__(self, interface):
# (https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Explained/Explained.html).
font_size = self.interface.size * 96 / 72

if self.interface.family == SYSTEM:
# Construct the NSFont
if font_family == SYSTEM:
font = NSFont.systemFontOfSize(font_size)
elif self.interface.family == MESSAGE:
elif font_family == MESSAGE:
font = NSFont.messageFontOfSize(font_size)
else:
family = {
SERIF: "Times-Roman",
SANS_SERIF: "Helvetica",
CURSIVE: "Apple Chancery",
FANTASY: "Papyrus",
MONOSPACE: "Courier New",
}.get(self.interface.family, self.interface.family)

font = NSFont.fontWithName(family, size=font_size)

if font is None:
print(
"Unable to load font: {}pt {}".format(
self.interface.size, family
)
)
font = NSFont.systemFontOfSize(font_size)
font = NSFont.fontWithName(custom_font_name, size=font_size)

# Convert the base font definition into a font with all the desired traits.
attributes_mask = 0
Expand All @@ -97,12 +118,12 @@ def __init__(self, interface):
attributes_mask |= NSFontMask.SmallCaps.value

if attributes_mask:
# If there is no font with the requested traits, this returns the original
# font unchanged.
font = NSFontManager.sharedFontManager.convertFont(
attributed_font = NSFontManager.sharedFontManager.convertFont(
font, toHaveTrait=attributes_mask
)
else:
attributed_font = font

_FONT_CACHE[self.interface] = font.retain()
_FONT_CACHE[self.interface] = attributed_font.retain()

self.native = font
self.native = attributed_font
101 changes: 9 additions & 92 deletions cocoa/src/toga_cocoa/libs/core_text.py
Original file line number Diff line number Diff line change
@@ -1,106 +1,23 @@
##########################################################################
# System/Library/Frameworks/CoreText.framework
##########################################################################
from ctypes import POINTER, c_bool, c_double, c_uint32, c_void_p, cdll, util

from rubicon.objc import CFIndex, CGFloat, CGGlyph, CGRect, CGSize, UniChar
from ctypes import c_bool, c_uint32, c_void_p, cdll, util

######################################################################
core_text = cdll.LoadLibrary(util.find_library("CoreText"))
######################################################################

######################################################################
# CTFontDescriptor.h

CTFontOrientation = c_uint32

######################################################################
# CTFontTraits.h

CTFontSymbolicTraits = c_uint32

######################################################################
# CTFont.h
core_text.CTFontGetBoundingRectsForGlyphs.restype = CGRect
core_text.CTFontGetBoundingRectsForGlyphs.argtypes = [
c_void_p,
CTFontOrientation,
POINTER(CGGlyph),
POINTER(CGRect),
CFIndex,
]

core_text.CTFontGetAdvancesForGlyphs.restype = c_double
core_text.CTFontGetAdvancesForGlyphs.argtypes = [
c_void_p,
CTFontOrientation,
POINTER(CGGlyph),
POINTER(CGSize),
CFIndex,
]

core_text.CTFontGetAscent.restype = CGFloat
core_text.CTFontGetAscent.argtypes = [c_void_p]

core_text.CTFontGetDescent.restype = CGFloat
core_text.CTFontGetDescent.argtypes = [c_void_p]

core_text.CTFontGetSymbolicTraits.restype = CTFontSymbolicTraits
core_text.CTFontGetSymbolicTraits.argtypes = [c_void_p]

core_text.CTFontGetGlyphsForCharacters.restype = c_bool
core_text.CTFontGetGlyphsForCharacters.argtypes = [
c_void_p,
POINTER(UniChar),
POINTER(CGGlyph),
CFIndex,
]

core_text.CTFontCreateWithGraphicsFont.restype = c_void_p
core_text.CTFontCreateWithGraphicsFont.argtypes = [
c_void_p,
CGFloat,
c_void_p,
c_void_p,
]

core_text.CTFontCopyFamilyName.restype = c_void_p
core_text.CTFontCopyFamilyName.argtypes = [c_void_p]

core_text.CTFontCopyFullName.restype = c_void_p
core_text.CTFontCopyFullName.argtypes = [c_void_p]

core_text.CTFontCreateWithFontDescriptor.restype = c_void_p
core_text.CTFontCreateWithFontDescriptor.argtypes = [c_void_p, CGFloat, c_void_p]

core_text.CTFontDescriptorCreateWithAttributes.restype = c_void_p
core_text.CTFontDescriptorCreateWithAttributes.argtypes = [c_void_p]

######################################################################
# CTFontDescriptor.h

kCTFontFamilyNameAttribute = c_void_p.in_dll(core_text, "kCTFontFamilyNameAttribute")
kCTFontTraitsAttribute = c_void_p.in_dll(core_text, "kCTFontTraitsAttribute")

######################################################################
# CTFontTraits.h

kCTFontSymbolicTrait = c_void_p.in_dll(core_text, "kCTFontSymbolicTrait")
kCTFontWeightTrait = c_void_p.in_dll(core_text, "kCTFontWeightTrait")

kCTFontItalicTrait = 1 << 0
kCTFontBoldTrait = 1 << 1

######################################################################
# CTLine.h

core_text.CTLineCreateWithAttributedString.restype = c_void_p
core_text.CTLineCreateWithAttributedString.argtypes = [c_void_p]

core_text.CTLineDraw.restype = None
core_text.CTLineDraw.argtypes = [c_void_p, c_void_p]
core_text.CTFontManagerRegisterFontsForURL.restype = c_bool
core_text.CTFontManagerRegisterFontsForURL.argtypes = [c_void_p, c_uint32, c_void_p]

######################################################################
# CTStringAttributes.h
# CTFontManagerScope.h

kCTFontAttributeName = c_void_p.in_dll(core_text, "kCTFontAttributeName")
kCTFontManagerScopeNone = 0
kCTFontManagerScopeProcess = 1
kCTFontManagerScopePersistent = 2
kCTFontManagerScopeSession = 3
kCTFontManagerScopeUser = 2
14 changes: 11 additions & 3 deletions cocoa/tests_backend/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@


class FontMixin:
supports_custom_fonts = False
supports_custom_fonts = True
supports_custom_variable_fonts = False

def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL):
# Cocoa's FANTASY (Papyrus) and CURSIVE (Apple Chancery) system
Expand Down Expand Up @@ -52,11 +53,18 @@ def assert_font_size(self, expected):

def assert_font_family(self, expected):
assert str(self.font.familyName) == {
# System and Message fonts use internal names
SYSTEM: ".AppleSystemUIFont",
MESSAGE: ".AppleSystemUIFont",
# Known fonts use pre-registered names
CURSIVE: "Apple Chancery",
FANTASY: "Papyrus",
MONOSPACE: "Courier New",
SANS_SERIF: "Helvetica",
SERIF: "Times",
SYSTEM: ".AppleSystemUIFont",
MESSAGE: ".AppleSystemUIFont",
# Most other fonts we can just use the family name;
# however, the Font Awesome font has a different
# internal Postscript name, which *doesn't* include
# the "solid" weight component.
"Font Awesome 5 Free Solid": "Font Awesome 5 Free",
}.get(expected, expected)
4 changes: 3 additions & 1 deletion docs/reference/api/resources/fonts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ properties to the ones used for widget styling::
Notes
-----

* macOS and iOS do not currently support registering user fonts.
* iOS and macOS do not support the use of variant font files (that is, fonts that
contain the details of multiple weights/variants in a single file). Variant font
files can be registered; however, only the "normal" variant will be used.

* Android and Windows do not support the oblique font style. If an oblique font is
specified, Toga will attempt to use an italic style of the same font.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/data/widgets_by_platform.csv
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ScrollContainer,Layout Widget,:class:`~toga.ScrollContainer`,A container that ca
SplitContainer,Layout Widget,:class:`~toga.SplitContainer`,A container that divides an area into two panels with a movable border,|y|,|y|,|y|,,,,
OptionContainer,Layout Widget,:class:`~toga.OptionContainer`,A container that can display multiple labeled tabs of content,|y|,|y|,|y|,,,,
App Paths,Resource,:class:`~toga.paths.Paths`,A mechanism for obtaining platform-appropriate filesystem locations for an application.,|y|,|y|,|y|,|y|,|y|,,|b|
Font,Resource,:class:`~toga.Font`,A text font,|b|,|y|,|y|,|b|,|y|,,
Font,Resource,:class:`~toga.Font`,A text font,|y|,|y|,|y|,|y|,|y|,,
Command,Resource,:class:`~toga.Command`,Command,|b|,|b|,|b|,,|b|,,
Group,Resource,:class:`~toga.Group`,Command group,|b|,|b|,|b|,|b|,|b|,,
Icon,Resource,:class:`~toga.Icon`,"A small, square image, used to provide easily identifiable visual context to a widget.",|y|,|y|,|y|,|y|,|y|,,|b|
Expand Down
1 change: 1 addition & 0 deletions gtk/tests_backend/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class FontMixin:
supports_custom_fonts = True
supports_custom_variable_fonts = True

def assert_font_family(self, expected):
assert self.font.get_family().split(",")[0] == expected
Expand Down
1 change: 1 addition & 0 deletions iOS/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
setup(
version=version,
install_requires=[
"fonttools >= 4.42.1, < 5.0.0",
"rubicon-objc >= 0.4.5rc1, < 0.5.0",
f"toga-core == {version}",
],
Expand Down
Loading

0 comments on commit 7a46d41

Please sign in to comment.