Skip to content

Commit

Permalink
Fix bold and italic for custom fonts #5 (#6)
Browse files Browse the repository at this point in the history
* Fix bold and italic for custom fonts #5

# Problem

Fix for issue #5

If a custom font is specified as the base font, strong and emphasis don't bold and italic respectively.

Example:

```Swift
let style = StyleSheet.default.duplicate().mutate(
        block: [
            .document: [
                .textStyle(.custom(name: .custom("Avenir-Medium"), size: .fixed(18))),
                .backgroundColor(.white),
                .textColor(.black)
            ],
        ])
```

There are two underlying bugs.

1. The first is `NS/UIFont.Weight` only works on system fonts regardless of how it's specified. So even though it can be combined in `NS/UIFontDescriptor.FontAttribute` it is ignored.
1. The second bug is how the symbolic traits (which is how italic is done) are modified on a `NS/UIFontDescriptor`. They have to be specified via `withSymbolTraits` builder method, and not manually built up in `NS/UIFontDescriptor.FontAttribute` dictionary. `withSymbolTraits` appears to have some magic logic in it that will actually change the font name based on if bold or italic are requested.

# Solution

First, remove `NativeFontWeight` as a way to specify bold, as it can't be made to work for anything other than system fonts. Second, change `FontTraits` to an `OptionSet` and add `.bold` as an option (in addition to the existing `.italic`). Italic is specified in the same way as before, but bold is now specified by setting the `.bold` `FontTraits`. Finally, change how `NS/UIFontDescriptor` are constructed to ensure they use `withSymbolTraits` instead of being manually constructed dictionaries.

The result is bolds are a bit less dark on iOS, but basically the same on macOS. Also added a snapshot spec for bolding/italicizing a custom font to help with regressions.  Several snapshot specs were updated because system updates caused differences in kerning. :-/

* Turn off snapshot specs since Github can't run them on a recent OS
  • Loading branch information
andyfinnell authored Apr 20, 2021
1 parent e8ef002 commit 0020061
Show file tree
Hide file tree
Showing 73 changed files with 93 additions and 127 deletions.
4 changes: 0 additions & 4 deletions Sources/NativeMarkKit/style/InlineStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public enum InlineStyle {
case strikethrough(NSUnderlineStyle, color: NativeColor? = nil)
case underline(NSUnderlineStyle, color: NativeColor? = nil)
case fontSize(CGFloat)
case fontWeight(NativeFontWeight)
case fontTraits(FontTraits)
case backgroundBorder(width: CGFloat = 1, color: NativeColor = .adaptableBlockQuoteMarginColor, sides: BorderSides = .all)
case inlineBackground(fillColor: NativeColor = .adaptableCodeBackgroundColor, strokeColor: NativeColor = .adaptableCodeBorderColor, strokeWidth: CGFloat = 1, cornerRadius: CGFloat = 3, topMargin: Length = 1.pt, bottomMargin: Length = 1.pt, leftMargin: Length = 6.pt, rightMargin: Length = 6.pt)
Expand Down Expand Up @@ -52,9 +51,6 @@ extension InlineStyle: ExpressibleAsAttributes {
case let .fontSize(fontSize):
let currentFont = attributes[.font] as? NativeFont
attributes[.font] = currentFont.withSize(fontSize)
case let .fontWeight(weight):
let currentFont = attributes[.font] as? NativeFont
attributes[.font] = currentFont.withWeight(weight)
case let .fontTraits(traits):
let currentFont = attributes[.font] as? NativeFont
attributes[.font] = currentFont.withTraits(traits)
Expand Down
2 changes: 0 additions & 2 deletions Sources/NativeMarkKit/style/NativeTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import AppKit

public typealias NativeColor = NSColor
public typealias NativeFont = NSFont
public typealias NativeFontWeight = NSFont.Weight
public typealias NativeImage = NSImage
public typealias NativeBezierPath = NSBezierPath
public typealias NativeFloat = CGFloat
Expand All @@ -23,7 +22,6 @@ import UIKit

public typealias NativeColor = UIColor
public typealias NativeFont = UIFont
public typealias NativeFontWeight = UIFont.Weight
public typealias NativeImage = UIImage
public typealias NativeBezierPath = UIBezierPath
public typealias NativeFloat = CGFloat
Expand Down
2 changes: 1 addition & 1 deletion Sources/NativeMarkKit/style/StyleSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public extension StyleSheet {
.fontTraits(.italic)
],
.strong: [
.fontWeight(.bold)
.fontTraits(.bold)
],
.code: [
.textStyle(.code),
Expand Down
82 changes: 28 additions & 54 deletions Sources/NativeMarkKit/style/TextStyle+AppKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ extension TextStyle {
if #available(OSX 10.15, *) {
return NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
} else {
return FontDescriptor(name: .systemMonospace, size: .fixed(12), weight: .regular, traits: .monospace).makeFont()
return FontDescriptor(name: .systemMonospace, size: .fixed(12), traits: .monospace).makeFont()
}
case let .custom(name, size, weight, traits):
let descriptor = FontDescriptor(name: name, size: size, weight: weight, traits: traits)
case let .custom(name, size, traits):
let descriptor = FontDescriptor(name: name, size: size, traits: traits)
return descriptor.makeFont()
}
}
Expand All @@ -47,66 +47,42 @@ extension Optional where Wrapped == NSFont {
func withSize(_ size: CGFloat) -> NSFont {
flatMap { $0.withSize(size) } ?? NSFont.systemFont(ofSize: size)
}

func withWeight(_ weight: NSFont.Weight) -> NSFont {
flatMap { $0.withWeight(weight) }
?? FontDescriptor(name: .system, size: .fixed(12), weight: weight, traits: .unspecified).makeFont()
}


func withTraits(_ traits: FontTraits) -> NSFont {
flatMap { $0.withTraits(traits) }
?? FontDescriptor(name: .system, size: .fixed(12), weight: .regular, traits: traits).makeFont()
?? FontDescriptor(name: .system, size: .fixed(12), traits: traits).makeFont()
}
}

extension NSFont {
func withSize(_ size: CGFloat) -> NSFont? {
NSFont(descriptor: fontDescriptor.withSize(size), textTransform: nil)
}

func withWeight(_ weight: NSFont.Weight) -> NSFont? {
NSFont(descriptor: fontDescriptor.withWeight(weight), textTransform: nil)
}


func withTraits(_ traits: FontTraits) -> NSFont? {
NSFont(descriptor: fontDescriptor.withTraits(traits), textTransform: nil)
}
}

private extension FontTraits {
var symbolicTraits: NSFontDescriptor.SymbolicTraits {
switch self {
case .italic:
return [.italic]
case .monospace:
return [.monoSpace]
case .unspecified:
return []
var traits: NSFontDescriptor.SymbolicTraits = []
if contains(.italic) {
traits.formUnion(.italic)
}
}

func updateTraits(_ traits: inout [NSFontDescriptor.TraitKey: Any]) {
let currentRawValue = traits[.symbolic] as? UInt32 ?? 0
let current = NSFontDescriptor.SymbolicTraits(rawValue: currentRawValue)
traits[.symbolic] = current.union(symbolicTraits).rawValue
if contains(.bold) {
traits.formUnion(.bold)
}
if contains(.monospace) {
traits.formUnion(.monoSpace)
}
return traits
}
}

private extension NSFontDescriptor {
func withWeight(_ weight: NSFont.Weight) -> NSFontDescriptor {
var attributes = fontAttributes
var traits = (attributes[.traits] as? [NSFontDescriptor.TraitKey: Any]) ?? [:]
traits[.weight] = weight
attributes[.traits] = traits
return NSFontDescriptor(fontAttributes: attributes)
}

func withTraits(_ fontTraits: FontTraits) -> NSFontDescriptor {
var attributes = fontAttributes
var traits = (attributes[.traits] as? [NSFontDescriptor.TraitKey: Any]) ?? [:]
fontTraits.updateTraits(&traits)
attributes[.traits] = traits
return NSFontDescriptor(fontAttributes: attributes)
withSymbolicTraits(symbolicTraits.union(fontTraits.symbolicTraits))
}
}

Expand All @@ -117,34 +93,32 @@ private extension FontDescriptor {
}

func descriptor() -> NSFontDescriptor {
var attributes = baseAttributes()
var traits = (attributes[.traits] as? [NSFontDescriptor.TraitKey: Any]) ?? [:]
traits[.weight] = weight
self.traits.updateTraits(&traits)
attributes[.traits] = traits
attributes[.size] = size.pointSize
return NSFontDescriptor(fontAttributes: attributes)
baseFontDescriptor().withTraits(traits)
}

func baseAttributes() -> [NSFontDescriptor.AttributeName: Any] {
func baseFontDescriptor() -> NSFontDescriptor {
switch name {
case .system:
return NSFont.systemFont(ofSize: size.pointSize)
.fontDescriptor
.fontAttributes
case .systemMonospace:
if #available(OSX 10.15, *) {
return NSFont.monospacedSystemFont(ofSize: size.pointSize, weight: weight)
return NSFont.monospacedSystemFont(ofSize: size.pointSize, weight: .regular)
.fontDescriptor
.fontAttributes
} else {
let traits: [NSFontDescriptor.TraitKey: Any] = [
.symbolic: NSFontMonoSpaceTrait
]
return [.traits: traits]
return NSFontDescriptor(fontAttributes: [
.traits: traits,
.size: size.pointSize
])
}
case let .custom(fontName):
return [.name: fontName]
return NSFontDescriptor(fontAttributes: [
.name: fontName,
.size: size.pointSize
])
}
}
}
Expand Down
85 changes: 29 additions & 56 deletions Sources/NativeMarkKit/style/TextStyle+UIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ extension TextStyle {
#if os(tvOS)
let title1Font = UIFont.preferredFont(forTextStyle: .title1)
let size = (title1Font.pointSize * 92.0 / 76.0).rounded()
let baseFont = UIFont.systemFont(ofSize: size)
return baseFont.withWeight(.medium) ?? baseFont
return UIFont.systemFont(ofSize: size, weight: .medium)
#else
if #available(iOS 11.0, watchOS 5.0, *) {
return UIFont.preferredFont(forTextStyle: .largeTitle)
Expand All @@ -46,10 +45,10 @@ extension TextStyle {
if #available(iOS 13.0, tvOS 13.0, *) {
return UIFont.monospacedSystemFont(ofSize: bodyFont.pointSize, weight: .regular)
} else {
return FontDescriptor(name: .systemMonospace, size: .fixed(bodyFont.pointSize), weight: .regular, traits: .monospace).makeFont()
return FontDescriptor(name: .systemMonospace, size: .fixed(bodyFont.pointSize), traits: .monospace).makeFont()
}
case let .custom(name, size, weight, traits):
let descriptor = FontDescriptor(name: name, size: size, weight: weight, traits: traits)
case let .custom(name, size, traits):
let descriptor = FontDescriptor(name: name, size: size, traits: traits)
return descriptor.makeFont()
}
}
Expand All @@ -63,62 +62,38 @@ extension Optional where Wrapped == UIFont {
func withSize(_ size: CGFloat) -> UIFont {
flatMap { $0.withSize(size) } ?? UIFont.systemFont(ofSize: size)
}

func withWeight(_ weight: UIFont.Weight) -> UIFont {
flatMap { $0.withWeight(weight) }
?? FontDescriptor(name: .system, size: .scaled(to: .body), weight: weight, traits: .unspecified).makeFont()
}


func withTraits(_ traits: FontTraits) -> UIFont {
flatMap { $0.withTraits(traits) }
?? FontDescriptor(name: .system, size: .scaled(to: .body), weight: .regular, traits: traits).makeFont()
?? FontDescriptor(name: .system, size: .scaled(to: .body), traits: traits).makeFont()
}
}

extension UIFont {
func withWeight(_ weight: UIFont.Weight) -> UIFont? {
UIFont(descriptor: fontDescriptor.withWeight(weight), size: pointSize)
}

func withTraits(_ traits: FontTraits) -> UIFont? {
UIFont(descriptor: fontDescriptor.withTraits(traits), size: pointSize)
UIFont(descriptor: fontDescriptor.withTraits(traits), size: 0)
}
}

private extension FontTraits {
var symbolicTraits: UIFontDescriptor.SymbolicTraits {
switch self {
case .italic:
return [.traitItalic]
case .monospace:
return [.traitMonoSpace]
case .unspecified:
return []
var traits: UIFontDescriptor.SymbolicTraits = []
if contains(.italic) {
traits.formUnion(.traitItalic)
}
}

func updateTraits(_ traits: inout [UIFontDescriptor.TraitKey: Any]) {
let currentRawValue = traits[.symbolic] as? UInt32 ?? 0
let current = UIFontDescriptor.SymbolicTraits(rawValue: currentRawValue)
traits[.symbolic] = current.union(symbolicTraits).rawValue
if contains(.bold) {
traits.formUnion(.traitBold)
}
if contains(.monospace) {
traits.formUnion(.traitMonoSpace)
}
return traits
}
}

private extension UIFontDescriptor {
func withWeight(_ weight: UIFont.Weight) -> UIFontDescriptor {
var attributes = fontAttributes
var traits = (attributes[.traits] as? [UIFontDescriptor.TraitKey: Any]) ?? [:]
traits[.weight] = weight
attributes[.traits] = traits
return UIFontDescriptor(fontAttributes: attributes)
}

func withTraits(_ fontTraits: FontTraits) -> UIFontDescriptor {
var attributes = fontAttributes
var traits = (attributes[.traits] as? [UIFontDescriptor.TraitKey: Any]) ?? [:]
fontTraits.updateTraits(&traits)
attributes[.traits] = traits
return UIFontDescriptor(fontAttributes: attributes)
withSymbolicTraits(symbolicTraits.union(fontTraits.symbolicTraits)) ?? self
}
}

Expand All @@ -128,34 +103,32 @@ private extension FontDescriptor {
}

func descriptor() -> UIFontDescriptor {
var attributes = baseAttributes()
var traits = (attributes[.traits] as? [UIFontDescriptor.TraitKey: Any]) ?? [:]
traits[.weight] = weight
self.traits.updateTraits(&traits)
attributes[.traits] = traits
attributes[.size] = size.pointSize
return UIFontDescriptor(fontAttributes: attributes)
baseFontDescriptor().withTraits(traits)
}

func baseAttributes() -> [UIFontDescriptor.AttributeName: Any] {
func baseFontDescriptor() -> UIFontDescriptor {
switch name {
case .system:
return UIFont.systemFont(ofSize: size.pointSize)
.fontDescriptor
.fontAttributes
case .systemMonospace:
if #available(iOS 13.0, tvOS 13.0, *) {
return UIFont.monospacedSystemFont(ofSize: size.pointSize, weight: weight)
return UIFont.monospacedSystemFont(ofSize: size.pointSize, weight: .regular)
.fontDescriptor
.fontAttributes
} else {
let traits: [UIFontDescriptor.TraitKey: Any] = [
.symbolic: UIFontDescriptor.SymbolicTraits.traitMonoSpace.rawValue
]
return [.traits: traits]
return UIFontDescriptor(fontAttributes: [
.traits: traits,
.size: size.pointSize
])
}
case let .custom(fontName):
return [.name: fontName]
return UIFontDescriptor(fontAttributes: [
.name: fontName,
.size: size.pointSize
])
}
}
}
Expand Down
21 changes: 13 additions & 8 deletions Sources/NativeMarkKit/style/TextStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ public enum FontName {
case custom(String)
}

public enum FontTraits {
case unspecified
case italic
case monospace
public struct FontTraits: OptionSet {
public let rawValue: Int

public init(rawValue: Int) {
self.rawValue = rawValue
}

public static let unspecified: FontTraits = []
public static let italic = FontTraits(rawValue: 1 << 0)
public static let bold = FontTraits(rawValue: 1 << 1)
public static let monospace = FontTraits(rawValue: 1 << 2)
}

public enum FontSize {
Expand All @@ -36,13 +43,11 @@ public enum FontSize {
public struct FontDescriptor {
public let name: FontName
public let size: FontSize
public let weight: NativeFontWeight
public let traits: FontTraits

public init(name: FontName, size: FontSize, weight: NativeFontWeight, traits: FontTraits) {
public init(name: FontName, size: FontSize, traits: FontTraits) {
self.name = name
self.size = size
self.weight = weight
self.traits = traits
}
}
Expand All @@ -60,5 +65,5 @@ public enum TextStyle {
case title2
case title3
case code
case custom(name: FontName = .system, size: FontSize = .scaled(to: .body), weight: NativeFontWeight = .regular, traits: FontTraits = .unspecified)
case custom(name: FontName = .system, size: FontSize = .scaled(to: .body), traits: FontTraits = .unspecified)
}
Loading

0 comments on commit 0020061

Please sign in to comment.