From 935b3d130021e82874a3cbb6fb1ad8a5e50c46a8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 6 May 2023 10:44:55 +0800 Subject: [PATCH 01/12] Initial implementation of Cococa font loading. --- cocoa/src/toga_cocoa/fonts.py | 107 ++++++++++++++++++------- cocoa/src/toga_cocoa/libs/core_text.py | 17 +++- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/cocoa/src/toga_cocoa/fonts.py b/cocoa/src/toga_cocoa/fonts.py index 619451cd4a..4c11e9d07d 100644 --- a/cocoa/src/toga_cocoa/fonts.py +++ b/cocoa/src/toga_cocoa/fonts.py @@ -1,10 +1,12 @@ from toga.fonts import ( + _REGISTERED_FONT_CACHE, BOLD, CURSIVE, FANTASY, ITALIC, MESSAGE, MONOSPACE, + NORMAL, SANS_SERIF, SERIF, SMALL_CAPS, @@ -12,6 +14,7 @@ SYSTEM_DEFAULT_FONT_SIZE, ) from toga_cocoa.libs import ( + NSURL, NSAttributedString, NSFont, NSFontAttributeName, @@ -19,8 +22,16 @@ NSFontMask, NSMutableDictionary, ) +from toga_cocoa.libs.core_text import core_text, kCTFontManagerScopeProcess _FONT_CACHE = {} +_POSTSCRIPT_NAMES = { + SERIF: "Times-Roman", + SANS_SERIF: "Helvetica", + CURSIVE: "Apple Chancery", + FANTASY: "Papyrus", + MONOSPACE: "Courier New", +} class Font: @@ -29,56 +40,96 @@ def __init__(self, interface): try: font = _FONT_CACHE[self.interface] except KeyError: + font = None + family = self.interface.family + font_key = self.interface.registered_font_key( + family, + weight=self.interface.weight, + style=self.interface.style, + variant=self.interface.variant, + ) + # Default system font size on Cocoa is 12pt if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE: font_size = NSFont.systemFontSize else: font_size = self.interface.size - if self.interface.family == SYSTEM: + if font_key in _REGISTERED_FONT_CACHE: + # print("LOAD", font_key, _REGISTERED_FONT_CACHE[font_key]) + font_path = str( + self.interface.factory.paths.app / _REGISTERED_FONT_CACHE[font_key] + ) + + font_url = NSURL.fileURLWithPath(font_path) + success = core_text.CTFontManagerRegisterFontsForURL( + font_url, kCTFontManagerScopeProcess, None + ) + if success: + # FIXME - this naming needs to be dynamically determined from the font, + # rather than hard-coded + _POSTSCRIPT_NAMES[family] = { + "awesome-free-solid": "Font Awesome 5 Free", + "Endor": "ENDOR", + }.get(family, family) + + if family == SYSTEM: font = NSFont.systemFontOfSize(font_size) - elif self.interface.family == MESSAGE: + elif family == MESSAGE: font = NSFont.messageFontOfSize(font_size) else: - if self.interface.family is SERIF: - family = "Times-Roman" - elif self.interface.family is SANS_SERIF: - family = "Helvetica" - elif self.interface.family is CURSIVE: - family = "Apple Chancery" - elif self.interface.family is FANTASY: - family = "Papyrus" - elif self.interface.family is MONOSPACE: - family = "Courier New" - else: - family = self.interface.family - - font = NSFont.fontWithName(family, size=self.interface.size) - - if font is None: - print( - "Unable to load font: {}pt {}".format( - self.interface.size, family - ) + try: + font = NSFont.fontWithName( + _POSTSCRIPT_NAMES[family], size=font_size ) - font = NSFont.systemFontOfSize(font_size) + except KeyError: + pass + + if font is None: + print( + f"Unknown font '{self.interface}'; " + "using system font as a fallback" + ) + font = NSFont.systemFontOfSize(font_size) # Convert the base font definition into a font with all the desired traits. attributes_mask = 0 if self.interface.weight == BOLD: attributes_mask |= NSFontMask.Bold.value + if self.interface.style == ITALIC: attributes_mask |= NSFontMask.Italic.value - if self.interface.variant == SMALL_CAPS: + elif self.interface.style == SMALL_CAPS: 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 ) + # print(font, attributed_font) + else: + attributed_font = font + + full_name = "{family}{weight}{style}".format( + family=family, + weight=(" " + self.interface.weight.title()) + if self.interface.weight is not NORMAL + else "", + style=(" " + self.interface.style.title()) + if self.interface.style is not NORMAL + else "", + ) + + if attributed_font is None: + print( + "Unable to load font: {}pt {}".format( + self.interface.size, full_name + ) + ) + else: + font = attributed_font - _FONT_CACHE[self.interface] = font.retain() + _FONT_CACHE[self.interface] = font self.native = font diff --git a/cocoa/src/toga_cocoa/libs/core_text.py b/cocoa/src/toga_cocoa/libs/core_text.py index b19cf09a31..3261ffe853 100644 --- a/cocoa/src/toga_cocoa/libs/core_text.py +++ b/cocoa/src/toga_cocoa/libs/core_text.py @@ -3,7 +3,7 @@ ########################################################################## 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 rubicon.objc import CFIndex, CGFloat, CGGlyph, CGRect, CGSize, NSArray, UniChar ###################################################################### core_text = cdll.LoadLibrary(util.find_library("CoreText")) @@ -76,12 +76,27 @@ core_text.CTFontDescriptorCreateWithAttributes.restype = c_void_p core_text.CTFontDescriptorCreateWithAttributes.argtypes = [c_void_p] +core_text.CTFontManagerRegisterFontsForURL.restype = c_bool +core_text.CTFontManagerRegisterFontsForURL.argtypes = [c_void_p, c_uint32, c_void_p] + +core_text.CTFontManagerCreateFontDescriptorsFromURL.restype = NSArray +core_text.CTFontManagerCreateFontDescriptorsFromURL.argtypes = [c_void_p] + ###################################################################### # CTFontDescriptor.h kCTFontFamilyNameAttribute = c_void_p.in_dll(core_text, "kCTFontFamilyNameAttribute") kCTFontTraitsAttribute = c_void_p.in_dll(core_text, "kCTFontTraitsAttribute") +###################################################################### +# CTFontManagerScope.h + +kCTFontManagerScopeNone = 0 +kCTFontManagerScopeProcess = 1 +kCTFontManagerScopePersistent = 2 +kCTFontManagerScopeSession = 3 +kCTFontManagerScopeUser = 2 + ###################################################################### # CTFontTraits.h From 305525b0fe08e3c41d7ddda87f434f6491815cf6 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 7 Apr 2023 10:00:36 +0800 Subject: [PATCH 02/12] Tweak the implementation to match #1846. --- changes/1837.feature.rst | 2 +- cocoa/src/toga_cocoa/fonts.py | 72 ++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/changes/1837.feature.rst b/changes/1837.feature.rst index 896050da51..39998511d7 100644 --- a/changes/1837.feature.rst +++ b/changes/1837.feature.rst @@ -1 +1 @@ -Support for custom font loading was added to the GTK backend. +Support for custom font loading was added to the GTK and Cococa backends. diff --git a/cocoa/src/toga_cocoa/fonts.py b/cocoa/src/toga_cocoa/fonts.py index 4c11e9d07d..e2b9e4459a 100644 --- a/cocoa/src/toga_cocoa/fonts.py +++ b/cocoa/src/toga_cocoa/fonts.py @@ -32,6 +32,15 @@ FANTASY: "Papyrus", MONOSPACE: "Courier New", } +SYSTEM_DEFAULT_FONTS = { + CURSIVE, + FANTASY, + MESSAGE, + MONOSPACE, + SANS_SERIF, + SERIF, + SYSTEM, +} class Font: @@ -49,31 +58,43 @@ def __init__(self, interface): variant=self.interface.variant, ) + # Font isn't a built-in system font, has been registered, but hasn't + # been loaded previously. + # FIXME this doesn't handle when there are multiple fonts in a file, + # or multiple font registrations for a single file. + if ( + self.interface.family not in SYSTEM_DEFAULT_FONTS + and font_key in _REGISTERED_FONT_CACHE + and self.interface.family not in _POSTSCRIPT_NAMES + ): + font_path = ( + self.interface.factory.paths.app / _REGISTERED_FONT_CACHE[font_key] + ) + if font_path.is_file(): + font_url = NSURL.fileURLWithPath(str(font_path)) + success = core_text.CTFontManagerRegisterFontsForURL( + font_url, kCTFontManagerScopeProcess, None + ) + if success: + # FIXME - this naming needs to be dynamically determined from the font, + # rather than hard-coded + _POSTSCRIPT_NAMES[self.interface.family] = { + "awesome-free-solid": "Font Awesome 5 Free", + "Endor": "ENDOR", + }.get(self.interface.family, self.interface.family) + else: + print(f"Font '{self.interface}' could not be loaded") + else: + print(f"Font file {font_path} could not be found") + # Default system font size on Cocoa is 12pt if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE: font_size = NSFont.systemFontSize else: font_size = self.interface.size - if font_key in _REGISTERED_FONT_CACHE: - # print("LOAD", font_key, _REGISTERED_FONT_CACHE[font_key]) - font_path = str( - self.interface.factory.paths.app / _REGISTERED_FONT_CACHE[font_key] - ) - - font_url = NSURL.fileURLWithPath(font_path) - success = core_text.CTFontManagerRegisterFontsForURL( - font_url, kCTFontManagerScopeProcess, None - ) - if success: - # FIXME - this naming needs to be dynamically determined from the font, - # rather than hard-coded - _POSTSCRIPT_NAMES[family] = { - "awesome-free-solid": "Font Awesome 5 Free", - "Endor": "ENDOR", - }.get(family, family) - - if family == SYSTEM: + # Construct the NSFont + if self.interface.family == SYSTEM: font = NSFont.systemFontOfSize(font_size) elif family == MESSAGE: font = NSFont.messageFontOfSize(font_size) @@ -83,14 +104,11 @@ def __init__(self, interface): _POSTSCRIPT_NAMES[family], size=font_size ) except KeyError: - pass - - if font is None: - print( - f"Unknown font '{self.interface}'; " - "using system font as a fallback" - ) - font = NSFont.systemFontOfSize(font_size) + print( + f"Unknown font '{self.interface}'; " + "using system font as a fallback" + ) + font = NSFont.systemFontOfSize(font_size) # Convert the base font definition into a font with all the desired traits. attributes_mask = 0 From a8f23c4e9825bfab4d379537dc0b9d8e6f59bc20 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 8 Oct 2023 09:34:25 +0800 Subject: [PATCH 03/12] Use fontTools to extract the font name from the font file. --- cocoa/setup.py | 1 + cocoa/src/toga_cocoa/fonts.py | 126 ++++++++---------- cocoa/tests_backend/fonts.py | 11 +- .../resources/canvas/write_text-macOS.png | Bin 14366 -> 18023 bytes 4 files changed, 66 insertions(+), 72 deletions(-) diff --git a/cocoa/setup.py b/cocoa/setup.py index 2f26f8f9d9..f9c2736edd 100644 --- a/cocoa/setup.py +++ b/cocoa/setup.py @@ -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}", ], diff --git a/cocoa/src/toga_cocoa/fonts.py b/cocoa/src/toga_cocoa/fonts.py index 7b533781ee..b17385c5a7 100644 --- a/cocoa/src/toga_cocoa/fonts.py +++ b/cocoa/src/toga_cocoa/fonts.py @@ -1,5 +1,7 @@ from pathlib import Path +from fontTools.ttLib import TTFont + from toga.fonts import ( _REGISTERED_FONT_CACHE, BOLD, @@ -8,14 +10,12 @@ ITALIC, MESSAGE, MONOSPACE, - NORMAL, OBLIQUE, SANS_SERIF, SERIF, SMALL_CAPS, SYSTEM, SYSTEM_DEFAULT_FONT_SIZE, - SYSTEM_DEFAULT_FONTS, ) from toga_cocoa.libs import ( NSURL, @@ -26,56 +26,71 @@ from toga_cocoa.libs.core_text import core_text, kCTFontManagerScopeProcess _FONT_CACHE = {} -_POSTSCRIPT_NAMES = { - SERIF: "Times-Roman", - SANS_SERIF: "Helvetica", - CURSIVE: "Apple Chancery", - FANTASY: "Papyrus", - MONOSPACE: "Courier New", -} +_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 = None - family = self.interface.family + font_family = self.interface.family font_key = self.interface._registered_font_key( - family, + family=font_family, weight=self.interface.weight, style=self.interface.style, variant=self.interface.variant, ) - # Font isn't a built-in system font, has been registered, but hasn't - # been loaded previously. - # FIXME this doesn't handle when there are multiple fonts in a file, - # or multiple font registrations for a single file. - if ( - self.interface.family not in SYSTEM_DEFAULT_FONTS - and font_key in _REGISTERED_FONT_CACHE - and self.interface.family not in _POSTSCRIPT_NAMES - ): - font_path = _REGISTERED_FONT_CACHE[font_key] - if Path(font_path).is_file(): - font_url = NSURL.fileURLWithPath(font_path) - success = core_text.CTFontManagerRegisterFontsForURL( - font_url, kCTFontManagerScopeProcess, None + try: + # 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: + 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" ) - if success: - # FIXME - this naming needs to be dynamically determined from the font, - # rather than hard-coded - _POSTSCRIPT_NAMES[self.interface.family] = { - "awesome-free-solid": "Font Awesome 5 Free", - "Endor": "ENDOR", - }.get(self.interface.family, self.interface.family) - else: - print(f"Font '{self.interface}' could not be loaded") + 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 @@ -86,21 +101,12 @@ def __init__(self, interface): font_size = self.interface.size * 96 / 72 # Construct the NSFont - if self.interface.family == SYSTEM: + if font_family == SYSTEM: font = NSFont.systemFontOfSize(font_size) - elif family == MESSAGE: + elif font_family == MESSAGE: font = NSFont.messageFontOfSize(font_size) else: - try: - font = NSFont.fontWithName( - _POSTSCRIPT_NAMES[family], size=font_size - ) - except KeyError: - print( - f"Unknown font '{self.interface}'; " - "using system font as a fallback" - ) - 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 @@ -116,29 +122,9 @@ def __init__(self, interface): attributed_font = NSFontManager.sharedFontManager.convertFont( font, toHaveTrait=attributes_mask ) - # print(font, attributed_font) else: attributed_font = font - full_name = "{family}{weight}{style}".format( - family=family, - weight=(" " + self.interface.weight.title()) - if self.interface.weight is not NORMAL - else "", - style=(" " + self.interface.style.title()) - if self.interface.style is not NORMAL - else "", - ) - - if attributed_font is None: - print( - "Unable to load font: {}pt {}".format( - self.interface.size, full_name - ) - ) - else: - font = attributed_font - - _FONT_CACHE[self.interface] = font.retain() + _FONT_CACHE[self.interface] = attributed_font.retain() - self.native = font + self.native = attributed_font diff --git a/cocoa/tests_backend/fonts.py b/cocoa/tests_backend/fonts.py index daa6ee8b3b..3acf2cc65d 100644 --- a/cocoa/tests_backend/fonts.py +++ b/cocoa/tests_backend/fonts.py @@ -52,11 +52,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) diff --git a/testbed/src/testbed/resources/canvas/write_text-macOS.png b/testbed/src/testbed/resources/canvas/write_text-macOS.png index a8253778cc5d329df23d9ce405b4475a5f8f26b0..4dcefb453c2e5d6e75d9628ce9a4c3a36bb68907 100644 GIT binary patch literal 18023 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�&DuNuAZ2aGxf`IKBGFcfC{K;5E~BdE)!__ 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 literal 14366 zcmc(GRa72Pvn3GR-QC^Y-JRg>4#C~sA&}q_9D;jrcXxM};BM2IduN{hnYHF=9-u!y zPMpSB=EaZt11Ts1k}S?OiWouOpI9B z+0nw<&Kv~9Gs`VWK(0?2Jv>)KVkSBgkra{OD?V*xaCGwGFFBMljEdGm7W=S51eq9g z&7Of@+r+I;98XBD7+d>+{VtWtN#1=EUY?hJNK}s9L>z8omAT+_nI0xd z-@cL5V%zx^r$er$8_Cb<``Ea1j&_~xVdj=|g_JLSKH60M#OPb-St92zUsE*DDkf?D z*^J=`<8g{(FYzSIy+0h-n<}fM@g(Ea+Uppd?P-VmR&L4s9+TXqHO2k&D)3viIp$ri z*L*5??D<>Pyu*q>OMfIXrSx~O-)Z|eDH%V;ZC=AW{n+;V3&z%pBgx{~ajwI~8KgDE zY-C-<2$SbStggI^rKP-~f5IVx&67kjdpTRi)E7G;@ zsQUqCpJ&BSFR%px0>Wt`BQEmQGwY(m%U6A&Qml7u`F96FRHlm^rc6(6}H zTIV-`!@W(%;=@P7i>=@3+-Awb&&_hb@}=^|CId@*f>Y&29f`wqW++tXpmDPFh-9{l zjvOI(RA?DQksk0V|1JX>PRs0$rA_VJ#>L0B&a<}A6jc)wlUb%wWzFt>Gj3DZ{(f@s zVqtlCQKSF-5$6|XodzO0;_NlC1cN*Apu&3X5bw`f5zdxYid*C@X?jyfJ z`#YJ!E4I-jO~ue8GDU)~c6>*y?zhK+kNB%2dwh($5FTR0WF{Y!MW%V7P z5qa=adc*2al%;Cp{S&HpFPZ=e3HO4>VvFKWr0$ZyR&6I4UphYV%6XU zlo!U)E9~J;)HH(%m%y1BIA z&xb}Y`W#VXYU|H3%x06vGlPbCVsB~>NPDx#0Dt2(EupB{w@r>xh{e9+c&KDDp0XUW zwmv4O>`|D+I4L|TBnCf}c0pv8O1upmSoJ`!)$-{&%`@C9GPK_=GIYr6EJrXb$wm5T zh$;E|PQ`GalemPXPu4GFxIUCD^p+O9E*lycqC4ifTNQA%Vu+YW9nF)N^<^AoA3vWu z_`!lHz{?B`^Y`pr2ps6<0|=-g^2|c(qdm3x*{f8`1Jp{zn?GVS?eM!LO2FHQ&0?2Z zq-{VV+qPdQX(^5UPKeN@#cr0_$DT(j8qQJSL1;utz#R;cU_IQsZEedgln`qAT>?#_ zOEmHJ67{GdBTp$?T;P1Sr5^Vy^mBlsyCQOnMO; zem4uS;Bg!m{8#r}CyJoctNRq&OHX@#xjD`dGL}AhB|Q-tWR0BO%Ji@>rrt5c#`5T# znZeC-crRd-A-(9sJd)5s6V^Qzm+v6w%Qf2?j>OXpVe|TZ54x;Va8p{S%V&8 zm1%4En1zfdc8`xQewLXDd6Ye~U0f6U=ge_;x55DkuXkH<>D*wsRDmgpIX|RdV#R0c zQLQViOa3(N#Xd@pL`umnw{;dST>_TwsfH2PgCCxTi>9<3#21+)QB6H2@%GbC6;P{q z)>JU++q&_3u(Z|kcfoXuLfOA8HQHN)@ZIZ#kWc5rrSUOTI-PBapiE~bOR5+s^Szlh zC@fB9rODY9bF1)sC!YIRtj0=7SeEX;$rKHEBC^lEkKiItXiY~2JZT2ePLZsN*54I< z>$5BMcF}b0X*+#`y1aX*$$78i_RqOt$rmFJoyb_HJM6bfX}oB45!tALV&^d?#>0!P zwUxU~<3dK2uRQ}?@(0nEQwKW3qk&?HyhV8lH;O%JW^w=hUK z8cct!pk}Ta+eMxXnK;3&h9_!|*n7Fxfsk;Cr0b3L3*@2=JzjWfQ+d4>cQbAL-65)p z&uH6qV1laGPu`;V(HpD5ZUHLOXxkF8?;$5hHK=Y`qdHC@6q~fwC)@t~J>o=@VDC z-@qyfK9iS~m8JarS!cV%C>e*#aeVpu?(csULNvX&7(TRT+!GAZe$@*nkSp}@wAcQ) z=|fIIp`xul67=QOsl{}%FB#sj*EE`fLPLr?DN5~W^`7qeZcb^&E&J{54YaGy``)JW zb>s5pdWgv7^Zl{j{X)l8|FYT6+?)af1H<=jM(S;?>m6^dM2U)(^~P5~0jaXGvO@3A zcO;=VlT8>Bo0+ zazai?S!X##h)J&%hZ| z45rQfqW8j`&1tuvX(wt@C^|pS8aJaD2Mka|2%yHMg=ea$JMqMPe3@=%OD^{ubTE)1 z_yXQ3>1>uc^d)EK>wl7~1K$7V@$JobvD$FzAk}b^Dd1TSEJ&D1Nku^cL0w&aJXa`p zh$tWnSk2BiZtsn@ii+5-&-csygM%3WKob^Z6ck|?bQ(3^c6tTX@T!-_Y^ysYU0m3r z^S`Z_r=q|^dhLW^aXamT)6mfH-%X2AhaAs@^bmhO4GC%0TL5lC(5lp!otyh}J%~3l zJPgf)!LLp%3jKsy9M>0DgChb0;2ALI`9?hy3AkE`qWSINM91j-(vq5%*6{8?%=3Bu z6gHOwJ|33?IUQY0csOKqOpMQuty)tkYisM44F~tX>m$H8pN-3%2qKH;n*#tfVKi#R z-!2-L>O8MaR@c`4oK>~8?1bZSyPs=S+cq^dIRj8mPE8%Uof4Xw;My9$Xx>sZGb5M# zNx$=aluLySoi#Jb0=3 zQCIhoOq@CcZ<&Un3p#E!6W3!;Pfz!KIVqvnZHfip#80TClzxA<#OAok9~~VH((-cp zTSi{q4A?GUq?XhpI&AodWui<@dr}ULAN%`eARk8o9}yR8&9^gBL^HFqy>;eeo&a)6 zD=RP3dj$aC@INe@>bXpCybT4sNfB|`KDs*eN22h*UG-JyG)4o&l>itZ0$(7whgj%k zCPAy#1md?wq9dIWywmb>(xP(| zX>QyTW77~2X>!h(?SFO{`^oTc*pshdJdu#1g$1R5SFV81!_{^O(#;4}F0<_-eSUs^ z8lMMauI~-Z*6Ze@=g0GrwXH1~E9>{xR^HLEv5}DxpkBizO8?w|LrFX4(kRW_nQev|N4?n;KD!XG;aXn zbpf3CZBBv9xKZcYyJOj|3A95KfPNu>jDNUkzZs=#znhh7zhBffGdBkvJbt(tW4awC zOSl?moBEf$njE(_k9ec=?;cFDBf3VuQJuGj<{<)G-P`U7Ic}Xyp;7ZbPu$%FXb;cF zE?JK84|w1hQ&(0tp9ibWJoULd-|NH07h*21)RYu-Jw_~_hw~Gs%PND8j3hFtI@c3b zLhoae&WFb8>qYDmt8xBW6VmSgr%szp;8ym-=?SiwaWfykSb1yE( zJ6n*}ErGblXqtVtFDwrdjZ%@Elr$JXUtN9u$ixIh?8(A>ncB4{e@bd+SmLhh%svT`7B)w9J) zdhNQvV<*l;MRsHiX`(^FSRm*_F6#m>znlg6Irpwk92&RVmhCE!j^ ze!_=~H7S38{{$b8o!+nx?}Z|{)H>rn$iwk;F>7m@2tl$6vV z?nPT(4ln^`PQZs(yMdz??#a$$zdpTQOXBzMsO<(!0PFBy_oInRN`6mF?B49~(eCv1 zs=kp+WxUX?2F!6(J46U%D2TQVMFRtagbpyH`jsnBesOW}?Q2ib@L*sofBP+DN{4&SoXp(}AECdMas*VR8dJQ_mJR2-> zW4c6uxMyc)mpgp8fXuYlxMX1Zcl{4KCg!etTROj&bM+0t0kH&pO+y>6r@yQJVGJNd z;9_QGW_KxZ2?<9)T30RK`}R}WjTp`fHZbb$k)vf)6*$QTdo znNlf7UPeZS=+TJ_$j$&o1I)kbNPbn6YA`Ct)X@*9pxyw0OY><@u~09=q?cjE)z!() z%{F+Qot=?Ifix`&U{1{4-95JhAFwSDe8AsxXqB1C$$c;PXFxPaR(pN4d)+QK+F}7B z7LW@!FK5+{x)J&LK+}!4A45~GCxgQ_4FDHo>X{1^kt~gh`j5LiS5C)Gc|}D;dwcuy z<=TR+8>X%|)|HhN!{Z!7w_XimqZS_*7Z({hxxKZm4^C}uZM)6(^fAV^imjWCzncJ= zblzrPCZ?y407A&SU4Mh#ururC#@X4$<ObM>^P{Y~`lzby?{0=|6$uSZ zRH~tWI>3$Lu3I@#e=qg@?iYNYb|R>wdkH+&dlCZPOh-pYy??x4^&xoy>G01*)0($r z!rbNB^mO+ZB>wJfkIj){g-k)sY)02!n_pM107obQD80YG|Hq^M1k>uO4j>(;9iPX? z<-e*Q0MfhtX*cGprlt$9PlIbsBl%i~u6Osp9ghYY)dmw}3H-eq9$N~4DR6Lb09vU7 z#2qUe+cQAreV6`^eC!H)#hDJgR8x(<&(0sOU7wj!M1Co+mtDC4>sGa%(lWN4v+nNh zQq$9uGchIbUA8Cx%UJxc=kqq z0KX{@fcWG9<>ua&{ck~*IbPvIgg&uCpU+eOQdS4Qq9`;FEE>Pvx=|!#qZVu~>Q~BM zU#0|KfnsQn$P3W=UO@hfijK}NDhm7Ka&*&=rgU{ulKVX~GiL7Od=K`)lOHfy$iLYm zi2TzU8raDk=*C6sdcDC>L~E>ONOQbTX#ip51<0>IM>#&Ct=a&Hoc)2>=Kv{end>*m zv7|S5i5om&u?Gy;I}-Zj(KPU)NHy@L0mK;z8yi~g6tF$MrvWDapnU4~=flhn(Z6BH z^9u`3mmQCFKzUNt`Em++_6g(_G-&YBva%EPOF-OS1F`80R9ClvNOZnm(5weC8808- z3J}(h>n=!G`mQKHe*Ad5N$mpYv{+LWwq#BQ@UYPHh*BDl3uRGJ(fns`;NzVrs+_#WMvTmfL5Aja zR8~IN?oPkgCm=iqfm~{3Yujhj_&p`1|K?y!`;W7E?y+9NP#>BSzlfOF7tYK$Ky}}x z4}$)avk8JP&|Oz*Por8dA(f^l~q+89%edPoXi%HY#nGO)nE;$Di9+JtE7;?5D8&D5-MKa zj1gCqXo>Z#>{q_fZoG<8RK&TWC9|QpgQ;|`vIXBNwsdsWWSF=S6{0!dj80Jy=8B^V zN)dFLI&0sF#7}P_%_8U!^fsTl5FvTi@HQWX<;c+R ztr?U%eatA4;!?tYNtSiL9qx(}yAn{wNlfNKyjWLJ#Y8V_1`hKDfBg#SGMY8->Z(r~ zG3T$8Ok$}8Ew07Tl79|8mX24C9Gqv^cX=Ho#9Z@?3@sXgmAMwb;J{NaZAUAz5Wivm zt~X_LvtTw1k#Eb}AzO^Fj{p-!=GSE4K&ed@ zG}ulIT;T0asf;laraE2%dVW?S1QO~st6%9gFn$Z%c}xLKnzv12(A;p$Y2~)RwjPL; zH6fsJd4^(er{G_B%W0J>Y0K4nEb8@Ya(YRypaW}kqvAbwu+8kVP?~zYa>%*$H1t#+ zn7($k)?p$ZX{h*?#;<<2$go+vc7KYKkO1Y3ke!F-B2&wkPEQe*bVs#T+pWzKu?vxq zrKaN+*<5svzCA6avoG!#h^dl6haff8CodXerQu6Ymm5OmqRl^QgIJ?0Wpt2KfpYkz zD!a~tc&NuqxWL+S#m`WxX0Y5nBe0M{P*u{I|D(7S(KiP~H5D4PT!F$nGzMiPDRvUO zCln`2K8@ux7`G@QOgIr$KiLRJkIRMAy%&q;pw&nU0`Ehkdxrp{fx|l%@}wqTiy@D8 zFoE&p3eu{=ar-FZooRw~o@^p)lV!J#;yTw9%r^j4-;{!NW(y`jZZ@3QTj5gk0e-s;s-| zUf0WPu>F^F%N;M+nVSrGcgh6Wm4e7%0~R8#U04#JWg%SBE~aP$5vRlNrO26Uqoqzx z<6SnSaMjuJ^Uy~}{!E6LePYCYv1k~TZ|@0h%l9%U{s}}Jt8wNxTKlEokuhM+aMtxW zwbQ8L%#l}~Dj92eRY67)3=%%SX;#$+20dU~KC1r6XA2!xoaUW}>TnI-lj?-wV_b}t|~<&`bLD(Zw$QI&DA=C~BB&3?=NvHsFMu#B%kkBKq%WM(FB z=9mN>boGKFH3rGyjDAslkiF%2B<(;Ut-MECJ1H*~j}JPNwCzq>xtMd3GR?l%(a_iz zMQLWv@IxZ5{l1OC?&Hh<&Rdp-Yq#pYNl&>NnkURD{Q>)ModQxRBPmZq;S?ha+Y zD}^rbNU1wJ+V?2jfJAY?#z0CQ6fHq&tiVMiJb2oq&3V}w((|+A)ZFAm`<3ODG9S$A zDA*J?Ww4T9qF!sRB%zoa85_;KXzRN2`+dd`7x* z+Qj5R5~{c}XFrc$)YH@OLv-A8S#D#mQl9Uh4!;U|LMV6$S*b*ISX<|e!P8Im>Do&= zu|S&PB;y0`*soqy%Fk4gFjdv`2I?QHi_1%sZ2yiU@<8Cr0yk26JMSs@KX2V!*}ZBM zWF;miLad|jKipupf1IQR8$IoTzyB_DNQ&MwdTE(k zODNWJwam-`&+VS~6N^YMp`-|HY+Pn|43k)@A3LAB0|jQrJfw6m0Vug=rLBFZEW~nC z!|K?f7OW4Fj~=g1#$Lr1O+N=!1^pe6FiS9;oDf+}?KH{YXJD1$grRMuo6)?k^-}t| z`?2(vXc)MA&bl^4nM(`Cr$oD~FlUXqaxUiwtD*Pwwdjo+vEf?EXZb`$WKF@<(C;cn zsK*ZTc`M%bRyNMH)!xERy*im8z+4-ip2oy7tp#S{c^L@&dgu!!F4j!xV5z}oc0^Lt z$>M(!N7ErIFbq-`VR@Ee1l6bTQ;zOrGFz26N=i03JW`Z(!K>ZF=s%B2V8e@|p zPuLH>F1tV{Wkzr-M^aadJbiiZO3e!wB*92ZmYJ@;?qO>KE)!7Youf2PgIV*k@CE&{ zdN6|%*6$-$?Qp&%k2dm9gZmqJ^zUhY12bhQYOnzZ37Kdq3kA`5ne*3;yzp`Ch}rdc zM<2pV{>EgY&y!O@TP!|7#{4^FhSOv4yYbi{DFk&9*BAJ;U+uUVYl%I?y$)ji5@bVh zoW-nVGR%SywB<_CC-mH{%^7d@=32W2GnNW0{xm)@VhwKHo4)(CunmYz-o#eij9(!Z z97Gf%gLvi=O{E_Fm@?u8DG2U8Ar2yxY647QZmUPR&w!2e8W6i3RNB4i3YL$BkB@+t z6gXB_mPeNB^uJ_-pXq)ZYL%YAQ8<_8+H@B(mpUyIGKkVqkND0;AYhAHf>$9OmGdJ= zM`NN^U}4|iIaXpx;qNo|_T2K#V_!Ho8&z=M;^*2XP-=`|*L1ew*CMwgs zZ>}0kWbr9^#R(#eX<>T2OW6AKHjZEXkk1VbgRA=z+y>DjUGcUq1wUj|@)`+hz3F<& z(ZmPaMzqo}W3-1miENf1N86?AF^;mBe(|5yQCS6_(_uE;xU4;km8O;;z8N z=rbT*`&5xuQ^0FFIXRXgd*vjR2r9+|c4vi9IsV6ll&&jRVfIPuYfNy;_r-8U&QAUA zp{WbX7C)h0V=+a-tu~h9S>&y|DCN2CVh=>N&+;fOJIN`7?}kzXN#64<)P2)`r-(Jt z#qfVOquQMqnLJow2H4Z?v7vnvU$(M6-~Z-7akj}XzOkc9Psrr_Ec%TPit`GJ_;?A3 zPQ(G@kQx1g-Ll{%w-kLZD|MwAYSI>mv0 zQQlq0hU6y`51ccQU$%sn_p2O(1l+_CbRXk^G4IR@qfwkf()sL*98Nmw#t|-X>-hJl zA7@xw`S6xTyp9_fW&Fjvohm7ro>Bem;&M|uODF@{Z6!i-g#<<`iYv19%i5wT*_k{`AKi( z_<(wKRw_zLYpX0v=kuAON%jR7r#|@wLrSR;L*!*##p(m3!AsG*xm6(0sNFIqx=QA) zlgX>?9tT@cl>jUnX<^s?(KwwM(XPoq)&#WZhZP|a&sK)w+wpMX5?Zv;$Wn>3ksph< z6NP-P8GGcMM)g6dXQ+A+c^4uL1!%6Mf)iTw1|*ru3vRtAw!63rPPxKPw60|i3pWdo zz)y)KrJaxx4-*<%4vp=p;8GODzn;gcWZ=hlN9cF-p7FSNb4M=vsmB@dRXCrrd~_XWaImQmX71O|0CLbAoCxr4^j&XWaMleR?^)k+J{ItwP!{;-$x^Lb^@d zRftQ*Min=eV-!^q;9$9|X6Z&OQ=_-VW$ao6gAypkq&`qG^%`IBNktHHm2Xaq&Ms?F zP7w%{wwf_19o1_hA(vLL;TCOh9$WvUo1MIqDEs37_Y7WDbFufjo!7lmP|+tDN=X;Q zF@nf3;@Z&?IJD))YY@3N6TrrxNJT4kTDBpi|AaSrQZxg)Hma%Y8qEb-mEo1sR9gu% zwqwPBm~|hN$7U%#<6Y+*l*bnVjK<#F8@W8^U>6z=9TE3XtjN3e*1+Kf1IaG#3SQTh z2qz(I%9g>^y=+YeMevrEhO6*)EKMt?qq52u3(?7h+`h6nFSA+9ifBiF@YKV*iYEZx zF7oeL8tBk+V)P*8SYgFquJ4Mn;HdbDk@3vnDr%PeOUzVu5UB8{4b7e?m=O36onmds z(A7u+$mFa%u$KMXxJAgwT(n>Ur3LPuuUltT<DHOd55irK5^>t;iZYM0<~H6FVnC@HR&+-*qEO>8wSUMBG+TYG)$Ts4Gwy z`xxw-gBENm3UwKb>TwYx+C`V0KzK}Ho&7Nze3Um|j%qR$@1=m&+bOD{W%uy_>V8)j1)g-R7HI!)%S|(~mltx$Q`CUjQ>+vPgd0TSDJO$TBw}0e@gqA*3ILrJ2 zV@55t{sk9AeJI28MJiWW+Y@Xp4SYXK-fWb*pvOZB-6L!+D;BEHlW?XZ-*a@rjo?kB zPq<4MS~rLq>}w1zZ@#)ZiYW3G)RaxKMt3z1g1E|yV;btTYbaRsz;YcOdJ!E6vLd6V z2MM^=*TO}P(`*PqrS7y_51}*S>xO5q6HUBQew_q2Ra|G8rdoqv*{BQa1Pv!h#5#O= z9eu;Hdq_F4NN_fhtKKy$v09ZkHdilk&JRpYbGKu}z7QrV-z2q?GQhV$;wBxw#EANs zQuX9*)}Q)Ou4c9mri>exY-!LG)pCide`~R)E)TXr8Po}yKk@JyJpobw^=Fdp*b*L+ z9oKdho^R~nxQp>i&Gm&ZtP5RDyo|6x76PN?1igDjg({6l&i3HQVYp3A~(+Q zhzy0N(C;=0yGd=0c| z!}Q%IeTSvYUrDiA%P&r)C>?P$DS9 zFxKx}vI=5fbA?fYui0XSB}j~XHS({$?qQ~16QpKKVuw%(dLEGCYYr}@AS)uOo4z%#O}|sw#t~a?OGZ3 z2^cQ=)k?rn8p0lX6J3DNesv2x?sWlT6N<*ebXFlMpGP-&$}Y8fGDU*#4s;+EhBBer zqM)A>6J}=gJVmy>U?IAeDryp(CEf_3qN7$h7=ps^+a5RR z1$~FLO|^uO1PcB83;PY{4-zB{99{DQG9RweDh>(1Lo;zv`kx^Z4H`=`?e(x_c-G1o zB^%PbECOG6NHHlMLB7rtTY50uAmj&8#>EUAHlS8GvOfIDb%>CjuBR->vC20Kz)1Mq z{(?F*h{`q_M>tHr1?CwDT4VvG}+zAp5V5WDC_6*j9NGwsvfhR=Ii$$=3&@#q+p~`TI%CHlSbVQ|LHPQ$1X#D$A=9>1? zL`6=VY6+`b54MFqZ+tcprpcNY$ouza{g}8CPDQd4K{AsIQrf*b@+`6vf_al-)kWALn;8OKN zW8+WhP+M^ik5we6%JE1rXJ;FqrIuc&Li4Su*B(*$HAd1#$h4o|dEUcgOUrxjJtm6w zw_#2bQcRq9rjMph18ip9h;UBAOQC_rnLaM3s2ZHfnSrTyQMqL!@zFQ!^kx&%fY-~) z($@rR918Wugfng4bIHS$3K{B3g6&(J`)_=4>K-M1%gvqUO!JFkcI@))HO1P-_Jtk= z)WLY4pU?8ziNRg%h}S0?Wc<3<5argtlr1JkUZ6tGwU_v9 z{iL@d4+7L!ur$eN(3qOP&Uu&L^S!aY&$d)ggv&>Y6_ewTS=%+B=nDbHl@khXhSpVg9|yhKW(m+pE1?b0JeP}G zQ|uNXfZxQU2(SjP1^Ch6xO|_~N*xa?Tfd$z>Zg?071Dwyst{Yz4Yac7!YwSvRt)@` zTcsj9&xKPkD1~>enPoH@{FT*$3df+Tp4=uh+3~G9jJtrq$_J^yZYAXErFC<_`m$M< zdLxPe$X*T-6&vCF8u90t8jr6hmzHVH#!9v98<7_$N1;MUn!Q+;UDR$h551SaVqX_!`JWQhZ1odqSBp!wkA zXy?TtBJqCimiwTX^iJlm1jA*ilOCOhax|c%5!Y)-N@M6XuV5(EpK0~)LWx{jkFIc( zbi5FTblX+S*S@*8*1MUoZXul#bauN7<^JoxD~o9V+vp5Rih5N|!RG>oH->iFZ6&!( zTpgoAkxm81fmPF@=D2kvHrIik-<$7Ea!O#DFt+Nk0RGDB;ozh{E_(lBsM&?Q8LY#C z$|ePmg>-Nk)cSHYclQx*Em(;i#qQpyRyWP(p1kvV590jI5(VfnL^{_%!$zMt21(q$ z%uJb7kEISk&>;U-pdeig!nIMPs{pTn<$OKz>E}s}qjFK9h2s2CXL;`w|F^QZpb9j= z8X=CU!$+h7;w0M33H)}7N78-xkrR$jm?ex z;m=+iRHRMdR8^amSJUUqzVHwBCy>D4Uf0Oq)gvzyPhxcfcalK~5iBdqe7nGSyKmO5 zUHwWnUzp3#4J|E8)T3nB>Y!Z0ezB!2tFH1z))k$Vv%Fdu9YSYy`WiZI^gP9q9)+O8KVY?)!A+pfAN@bI}O zi-2ol)CDYuG7)~3a#N|?O0t=6ZnH*bG05!g)~uqt#zeF7uB5F!vbx0fBOE}u@!uvd ztl#ZHkjKT~TzN+{sb3uy=+1=jj?Q1dS5F<|=C>kBh_24}6hWXy8;PrulYO7GaC}?I z*|LJ|-)EmK9z-L!%&(n^$>Lbi@)B$mp@Yr;g)^36kFVvERTqqArqBNqD6~z`$ZA&9 z!TjUUFzoSH)JK4qDxOw-OVx%7lw>ZsaJUdAj*Aw=``gl*jJ{dN^&+Sd_k|8~Q?dPw zKd4WAEI1^PV2nA-X8gL8bj?y@g7S0Yxq*<$^4;)u`R-7p!W~W&LwX}JVar_ zrnQ;5kYAD#^t5YA2gH-E!1#v+W>ndX*v)z$0Wfo*O$M^$8VT zb1_)~X_DL#X|Ynjjy9A6gB4-5ilRzQ3xOp4a#|Hco1iTR7feJSz4{p0r_f1NBjGed&7i}iXwYY11_BUZ@nk@9OHwAUgxmxB-Q_u}1ZxAErw+_crp7x*T^;|0OjL(ChC+&pP@zWz4X zd#IFm|EP~_pD3EwY}l23WrA{q^G1HgErwtxSGLx z?Gs|9|EI135e|g~(L_ojF%7eCxeO*Y2U$uy7J;T`FSXB_pvsV6aHYe`c^Ku!uHbo+ zgfMsiLo?&}N_PFB9!Uj>3Ve{ZDWA5qTzHOLq$?bAei|r9+XOXZ?ZP!Oqi59>ms;xx z5bwp{F<}STkCOX=*MlkT+ABc>EqIi|4QSko&y6_ew From b91c621ccff039c126876819efab7bc1735a24a8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 8 Oct 2023 10:00:38 +0800 Subject: [PATCH 04/12] Simplify core-text wrapper. --- cocoa/src/toga_cocoa/fonts.py | 1 - cocoa/src/toga_cocoa/libs/core_text.py | 100 +------------------------ 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/cocoa/src/toga_cocoa/fonts.py b/cocoa/src/toga_cocoa/fonts.py index b17385c5a7..a8a902ab64 100644 --- a/cocoa/src/toga_cocoa/fonts.py +++ b/cocoa/src/toga_cocoa/fonts.py @@ -35,7 +35,6 @@ def __init__(self, interface): try: attributed_font = _FONT_CACHE[self.interface] except KeyError: - font = None font_family = self.interface.family font_key = self.interface._registered_font_key( family=font_family, diff --git a/cocoa/src/toga_cocoa/libs/core_text.py b/cocoa/src/toga_cocoa/libs/core_text.py index 3261ffe853..a109cbf175 100644 --- a/cocoa/src/toga_cocoa/libs/core_text.py +++ b/cocoa/src/toga_cocoa/libs/core_text.py @@ -1,93 +1,18 @@ ########################################################################## # 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, NSArray, 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] core_text.CTFontManagerRegisterFontsForURL.restype = c_bool core_text.CTFontManagerRegisterFontsForURL.argtypes = [c_void_p, c_uint32, c_void_p] -core_text.CTFontManagerCreateFontDescriptorsFromURL.restype = NSArray -core_text.CTFontManagerCreateFontDescriptorsFromURL.argtypes = [c_void_p] - -###################################################################### -# CTFontDescriptor.h - -kCTFontFamilyNameAttribute = c_void_p.in_dll(core_text, "kCTFontFamilyNameAttribute") -kCTFontTraitsAttribute = c_void_p.in_dll(core_text, "kCTFontTraitsAttribute") - ###################################################################### # CTFontManagerScope.h @@ -96,26 +21,3 @@ kCTFontManagerScopePersistent = 2 kCTFontManagerScopeSession = 3 kCTFontManagerScopeUser = 2 - -###################################################################### -# 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] - -###################################################################### -# CTStringAttributes.h - -kCTFontAttributeName = c_void_p.in_dll(core_text, "kCTFontAttributeName") From 863bbf2830e84a6c733fa6eb4c767cbd2a7b46bd Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 8 Oct 2023 14:32:03 +0800 Subject: [PATCH 05/12] Add iOS core-text implementation of custom fonts. --- iOS/setup.py | 1 + iOS/src/toga_iOS/fonts.py | 98 +++++++++++------- iOS/src/toga_iOS/libs/core_text.py | 23 ++++ .../resources/canvas/write_text-iOS.png | Bin 13933 -> 17276 bytes 4 files changed, 85 insertions(+), 37 deletions(-) create mode 100644 iOS/src/toga_iOS/libs/core_text.py diff --git a/iOS/setup.py b/iOS/setup.py index 2f26f8f9d9..f9c2736edd 100644 --- a/iOS/setup.py +++ b/iOS/setup.py @@ -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}", ], diff --git a/iOS/src/toga_iOS/fonts.py b/iOS/src/toga_iOS/fonts.py index 3a93806bcb..27311877f8 100644 --- a/iOS/src/toga_iOS/fonts.py +++ b/iOS/src/toga_iOS/fonts.py @@ -1,5 +1,7 @@ from pathlib import Path +from fontTools.ttLib import TTFont + from toga.fonts import ( _REGISTERED_FONT_CACHE, BOLD, @@ -13,46 +15,80 @@ SERIF, SYSTEM, SYSTEM_DEFAULT_FONT_SIZE, - SYSTEM_DEFAULT_FONTS, ) from toga_iOS.libs import ( + NSURL, UIFont, UIFontDescriptorTraitBold, UIFontDescriptorTraitItalic, ) +from toga_iOS.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: "Snell Roundhand", + 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: size = UIFont.labelFontSize @@ -62,23 +98,12 @@ def __init__(self, interface): # (https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Explained/Explained.html). size = self.interface.size * 96 / 72 - if self.interface.family == SYSTEM: - base_font = UIFont.systemFontOfSize(size) - elif self.interface.family == MESSAGE: - base_font = UIFont.systemFontOfSize(size) + if font_family == SYSTEM: + font = UIFont.systemFontOfSize(size) + elif font_family == MESSAGE: + font = UIFont.systemFontOfSize(size) else: - family = { - SERIF: "Times-Roman", - SANS_SERIF: "Helvetica", - CURSIVE: "Snell Roundhand", - FANTASY: "Papyrus", - MONOSPACE: "Courier New", - }.get(self.interface.family, self.interface.family) - - base_font = UIFont.fontWithName(family, size=size) - if base_font is None: - print(f"Unable to load font: {size}pt {family}") - base_font = UIFont.systemFontOfSize(size) + font = UIFont.fontWithName(custom_font_name, size=size) # Convert the base font definition into a font with all the desired traits. traits = 0 @@ -90,16 +115,15 @@ def __init__(self, interface): if traits: # If there is no font with the requested traits, this returns the original # font unchanged. - font = UIFont.fontWithDescriptor( - base_font.fontDescriptor.fontDescriptorWithSymbolicTraits(traits), + attributed_font = UIFont.fontWithDescriptor( + font.fontDescriptor.fontDescriptorWithSymbolicTraits(traits), size=size, ) - # If the traits conversion failed, fall back to the default font. - if font is None: - font = base_font + if attributed_font is None: + attributed_font = font else: - font = base_font + attributed_font = font - _FONT_CACHE[self.interface] = font.retain() + _FONT_CACHE[self.interface] = attributed_font.retain() - self.native = font + self.native = attributed_font diff --git a/iOS/src/toga_iOS/libs/core_text.py b/iOS/src/toga_iOS/libs/core_text.py new file mode 100644 index 0000000000..a109cbf175 --- /dev/null +++ b/iOS/src/toga_iOS/libs/core_text.py @@ -0,0 +1,23 @@ +########################################################################## +# System/Library/Frameworks/CoreText.framework +########################################################################## +from ctypes import c_bool, c_uint32, c_void_p, cdll, util + +###################################################################### +core_text = cdll.LoadLibrary(util.find_library("CoreText")) +###################################################################### + +###################################################################### +# CTFont.h + +core_text.CTFontManagerRegisterFontsForURL.restype = c_bool +core_text.CTFontManagerRegisterFontsForURL.argtypes = [c_void_p, c_uint32, c_void_p] + +###################################################################### +# CTFontManagerScope.h + +kCTFontManagerScopeNone = 0 +kCTFontManagerScopeProcess = 1 +kCTFontManagerScopePersistent = 2 +kCTFontManagerScopeSession = 3 +kCTFontManagerScopeUser = 2 diff --git a/testbed/src/testbed/resources/canvas/write_text-iOS.png b/testbed/src/testbed/resources/canvas/write_text-iOS.png index bbabc836f7be5c5e969427ca3a49279e1202687f..149d8b12baf0d111009a3ee463692f5bb1bb17a1 100644 GIT binary patch literal 17276 zcmajH1yohhw>L_699jkGlt$_920>c7LAo31lJ0IqIz<|!yF(`aqhU01VE^~=%V}$c+rCgxBDK3vtMLXw>4PfMHbe{fG7 z$j^W*-QTuN?V8TrE=Y}x+=y&|N|t)P-?NW*l=Z8WQwulJ-% z1>)CsjBiCr)kDjq0z>-MFNIAr-*xNk_86;$9^v};*j(NJ5#OhtAou)9hzy4LGpr6U znvS%8LbgkRza1lf-ecPIfk;kSmb?PHFx45XI!Q85OsueZP zV%SK2Zi+k#_nO>IQpxf}hLG|wOTglKw|1yW= z`^w9*N3PV4kG$c#Ytdc@nf#5Hk^!eGcjF~mjqPJ-75u?M6>wx9MTq1)%%NB_{dXs6 z($bHl$!Ui%g!*i-hTW_nQ6K3adtFU;rUT!cYNJJrs3^)k3B3RH^Oa~By*SPd-@7;l zvVlv)hqMwhY3xb(cxd^V`KAZBke^xNaa2v{=6%1KJq>>D35K<^WCn}4j#6&d>_+4# z5Hwe5;04Rc8vm0&W%TshLg;jRt0QhK2*dgruf31y_OC)xTYWs)C!AOAvYc(VC!{0~ z7E@H1*%-Ep+sukyK@{_Y(;;`UTvhzfKBv61kK3fwwYf3$Jv+h`*fk8FVpK=B3MHLU z%f=a{HmR7rbnBzo>3GXioS&t)p3EfQJ6Xa≠F%-yeEV3yw#Rwvwki$;;z0JkJrY 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 literal 13933 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 Date: Sun, 8 Oct 2023 15:00:42 +0800 Subject: [PATCH 06/12] Modify font tests to cover a font that has multiple weight descriptions. --- testbed/tests/test_fonts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testbed/tests/test_fonts.py b/testbed/tests/test_fonts.py index 6eb33763f1..ad6367edc7 100644 --- a/testbed/tests/test_fonts.py +++ b/testbed/tests/test_fonts.py @@ -82,8 +82,10 @@ async def test_font_options(widget: toga.Label, font_probe): "resources/fonts/Font Awesome 5 Free-Solid-900.otf", {"weight": BOLD}, ), - # TrueType font, no options + # TrueType font supporting multiple styles, no options ("Endor", "resources/fonts/ENDOR___.ttf", {}), + # TrueType font supporting multiple styles, with options + ("Endor", "resources/fonts/ENDOR___.ttf", {"weight": BOLD}), # Font with weight property ("Roboto", "resources/fonts/Roboto-Bold.ttf", {"weight": BOLD}), # Font with style property From 5a5a48a84c658c09fc897bc79b4c7a2b440125db Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 8 Oct 2023 09:52:12 +0200 Subject: [PATCH 07/12] Enable iOS custom font tests. --- cocoa/src/toga_cocoa/fonts.py | 2 ++ docs/reference/api/resources/fonts.rst | 3 ++- iOS/tests_backend/fonts.py | 13 ++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cocoa/src/toga_cocoa/fonts.py b/cocoa/src/toga_cocoa/fonts.py index a8a902ab64..96e11d2374 100644 --- a/cocoa/src/toga_cocoa/fonts.py +++ b/cocoa/src/toga_cocoa/fonts.py @@ -121,6 +121,8 @@ def __init__(self, interface): attributed_font = NSFontManager.sharedFontManager.convertFont( font, toHaveTrait=attributes_mask ) + if attributed_font is None: + attributed_font = font else: attributed_font = font diff --git a/docs/reference/api/resources/fonts.rst b/docs/reference/api/resources/fonts.rst index fd8171c22f..b349faaefa 100644 --- a/docs/reference/api/resources/fonts.rst +++ b/docs/reference/api/resources/fonts.rst @@ -71,7 +71,8 @@ properties to the ones used for widget styling:: Notes ----- -* macOS and iOS do not currently support registering user fonts. +* macOS and iOS do not currently support variable font files (a single font file that + contains all the details for multiple font variants and weights). * 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. diff --git a/iOS/tests_backend/fonts.py b/iOS/tests_backend/fonts.py index e13f1017ec..cec979331e 100644 --- a/iOS/tests_backend/fonts.py +++ b/iOS/tests_backend/fonts.py @@ -20,7 +20,7 @@ class FontMixin: - supports_custom_fonts = False + supports_custom_fonts = True def assert_font_options(self, weight=NORMAL, style=NORMAL, variant=NORMAL): # Cocoa's FANTASY (Papyrus) and CURSIVE (Snell Roundhand) system @@ -55,11 +55,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: "Snell Roundhand", FANTASY: "Papyrus", MONOSPACE: "Courier New", SANS_SERIF: "Helvetica", SERIF: "Times New Roman", - 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) From 9240da395b8afc4dff582cc49abaa76c6140bd74 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 8 Oct 2023 10:59:07 +0200 Subject: [PATCH 08/12] Disable variable font tests on cocoa and iOS. --- android/tests_backend/fonts.py | 1 + cocoa/tests_backend/fonts.py | 1 + docs/reference/api/resources/fonts.rst | 3 --- docs/reference/data/widgets_by_platform.csv | 2 +- gtk/tests_backend/fonts.py | 1 + iOS/tests_backend/fonts.py | 1 + testbed/tests/test_fonts.py | 15 ++++++++++----- winforms/tests_backend/fonts.py | 1 + 8 files changed, 16 insertions(+), 9 deletions(-) diff --git a/android/tests_backend/fonts.py b/android/tests_backend/fonts.py index 63014f7875..600444add0 100644 --- a/android/tests_backend/fonts.py +++ b/android/tests_backend/fonts.py @@ -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 diff --git a/cocoa/tests_backend/fonts.py b/cocoa/tests_backend/fonts.py index 3acf2cc65d..ec98b782a2 100644 --- a/cocoa/tests_backend/fonts.py +++ b/cocoa/tests_backend/fonts.py @@ -18,6 +18,7 @@ class FontMixin: 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 diff --git a/docs/reference/api/resources/fonts.rst b/docs/reference/api/resources/fonts.rst index b349faaefa..00b11c2e85 100644 --- a/docs/reference/api/resources/fonts.rst +++ b/docs/reference/api/resources/fonts.rst @@ -71,9 +71,6 @@ properties to the ones used for widget styling:: Notes ----- -* macOS and iOS do not currently support variable font files (a single font file that - contains all the details for multiple font variants and weights). - * 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. diff --git a/docs/reference/data/widgets_by_platform.csv b/docs/reference/data/widgets_by_platform.csv index 229fea87e1..4d72686d92 100644 --- a/docs/reference/data/widgets_by_platform.csv +++ b/docs/reference/data/widgets_by_platform.csv @@ -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| diff --git a/gtk/tests_backend/fonts.py b/gtk/tests_backend/fonts.py index 59c7ece09c..d09da437f6 100644 --- a/gtk/tests_backend/fonts.py +++ b/gtk/tests_backend/fonts.py @@ -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 diff --git a/iOS/tests_backend/fonts.py b/iOS/tests_backend/fonts.py index cec979331e..b3b38fb1fe 100644 --- a/iOS/tests_backend/fonts.py +++ b/iOS/tests_backend/fonts.py @@ -21,6 +21,7 @@ class FontMixin: 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 (Snell Roundhand) system diff --git a/testbed/tests/test_fonts.py b/testbed/tests/test_fonts.py index ad6367edc7..4a83d1c1f3 100644 --- a/testbed/tests/test_fonts.py +++ b/testbed/tests/test_fonts.py @@ -74,27 +74,29 @@ async def test_font_options(widget: toga.Label, font_probe): @pytest.mark.parametrize( - "font_family,font_path,font_kwargs", + "font_family,font_path,font_kwargs,variable_font_test", [ # OpenType font with weight property ( "Font Awesome 5 Free Solid", "resources/fonts/Font Awesome 5 Free-Solid-900.otf", {"weight": BOLD}, + False, ), # TrueType font supporting multiple styles, no options - ("Endor", "resources/fonts/ENDOR___.ttf", {}), + ("Endor", "resources/fonts/ENDOR___.ttf", {}, False), # TrueType font supporting multiple styles, with options - ("Endor", "resources/fonts/ENDOR___.ttf", {"weight": BOLD}), + ("Endor", "resources/fonts/ENDOR___.ttf", {"weight": BOLD}, True), # Font with weight property - ("Roboto", "resources/fonts/Roboto-Bold.ttf", {"weight": BOLD}), + ("Roboto", "resources/fonts/Roboto-Bold.ttf", {"weight": BOLD}, False), # Font with style property - ("Roboto", "resources/fonts/Roboto-Italic.ttf", {"style": ITALIC}), + ("Roboto", "resources/fonts/Roboto-Italic.ttf", {"style": ITALIC}, False), # Font with multiple properties ( "Roboto", "resources/fonts/Roboto-BoldItalic.ttf", {"weight": BOLD, "style": ITALIC}, + False, ), ], ) @@ -105,6 +107,7 @@ async def test_font_file_loaded( font_family: str, font_path: str, font_kwargs, + variable_font_test: bool, capsys: pytest.CaptureFixture[str], ): """Custom fonts can be loaded and used.""" @@ -116,6 +119,8 @@ async def test_font_file_loaded( if not font_probe.supports_custom_fonts: pytest.skip("Platform doesn't support loading custom fonts") + if variable_font_test and not font_probe.supports_custom_variable_fonts: + pytest.skip("Platform doesn't support loading custom variable fonts") # Update widget font family and other options if needed widget.style.font_family = font_family diff --git a/winforms/tests_backend/fonts.py b/winforms/tests_backend/fonts.py index a883d77735..9018b846bc 100644 --- a/winforms/tests_backend/fonts.py +++ b/winforms/tests_backend/fonts.py @@ -19,6 +19,7 @@ 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.font.Bold else NORMAL == weight From 2edb553cffed067913836489c265ad69dcae939e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 8 Oct 2023 11:21:49 +0200 Subject: [PATCH 09/12] Restrict the scope of what is skipped by the variadic font tests. --- cocoa/src/toga_cocoa/fonts.py | 2 -- iOS/src/toga_iOS/fonts.py | 3 +-- testbed/tests/test_fonts.py | 7 ++++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cocoa/src/toga_cocoa/fonts.py b/cocoa/src/toga_cocoa/fonts.py index 96e11d2374..a8a902ab64 100644 --- a/cocoa/src/toga_cocoa/fonts.py +++ b/cocoa/src/toga_cocoa/fonts.py @@ -121,8 +121,6 @@ def __init__(self, interface): attributed_font = NSFontManager.sharedFontManager.convertFont( font, toHaveTrait=attributes_mask ) - if attributed_font is None: - attributed_font = font else: attributed_font = font diff --git a/iOS/src/toga_iOS/fonts.py b/iOS/src/toga_iOS/fonts.py index 27311877f8..e34dfa830d 100644 --- a/iOS/src/toga_iOS/fonts.py +++ b/iOS/src/toga_iOS/fonts.py @@ -113,8 +113,7 @@ def __init__(self, interface): traits |= UIFontDescriptorTraitItalic if traits: - # If there is no font with the requested traits, this returns the original - # font unchanged. + # If there is no font with the requested traits, this returns None. attributed_font = UIFont.fontWithDescriptor( font.fontDescriptor.fontDescriptorWithSymbolicTraits(traits), size=size, diff --git a/testbed/tests/test_fonts.py b/testbed/tests/test_fonts.py index 4a83d1c1f3..c271089b62 100644 --- a/testbed/tests/test_fonts.py +++ b/testbed/tests/test_fonts.py @@ -119,8 +119,6 @@ async def test_font_file_loaded( if not font_probe.supports_custom_fonts: pytest.skip("Platform doesn't support loading custom fonts") - if variable_font_test and not font_probe.supports_custom_variable_fonts: - pytest.skip("Platform doesn't support loading custom variable fonts") # Update widget font family and other options if needed widget.style.font_family = font_family @@ -132,7 +130,10 @@ async def test_font_file_loaded( # Check that font properties are updated font_probe.assert_font_family(font_family) - font_probe.assert_font_options(**font_kwargs) + # Only check the font options if this is a non-variable font test, or the backend + # supports variable fonts + if not variable_font_test or font_probe.supports_custom_variable_fonts: + font_probe.assert_font_options(**font_kwargs) # Setting the font to "Roboto something" involves setting the font to # "Roboto" as an intermediate step. However, we haven't registered "Roboto From 736cb4ce63f04777be615b8d49b7bd3e642b1287 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 10 Oct 2023 15:44:41 +0200 Subject: [PATCH 10/12] Corrected typo --- changes/1837.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1837.feature.rst b/changes/1837.feature.rst index 39998511d7..86d514388d 100644 --- a/changes/1837.feature.rst +++ b/changes/1837.feature.rst @@ -1 +1 @@ -Support for custom font loading was added to the GTK and Cococa backends. +Support for custom font loading was added to the GTK and Cocoa backends. From c26d200907c1c82ae6204c3a7e0bb6e9e6cc62e0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 10 Oct 2023 15:45:06 +0200 Subject: [PATCH 11/12] Include iOS in the release note. --- changes/1837.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/1837.feature.rst b/changes/1837.feature.rst index 86d514388d..e9b4941620 100644 --- a/changes/1837.feature.rst +++ b/changes/1837.feature.rst @@ -1 +1 @@ -Support for custom font loading was added to the GTK and Cocoa backends. +Support for custom font loading was added to the GTK, Cocoa and iOS backends. From e67de2e8fbc79e419820b54d3dc9d40f7947c530 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 10 Oct 2023 15:50:05 +0200 Subject: [PATCH 12/12] Added platform note about cocoa/iOS variant font files. --- docs/reference/api/resources/fonts.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/api/resources/fonts.rst b/docs/reference/api/resources/fonts.rst index 00b11c2e85..86bd1b35ff 100644 --- a/docs/reference/api/resources/fonts.rst +++ b/docs/reference/api/resources/fonts.rst @@ -71,6 +71,10 @@ properties to the ones used for widget styling:: Notes ----- +* 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.