| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- extension Font {
- /// A dynamic text style based off [Apple's typography guidelines](https://developer.apple.com/design/human-interface-guidelines/typography).
- public enum TextStyle: Hashable, Sendable, CaseIterable, Codable {
- /// The font style for large titles.
- case largeTitle
- /// The font used for first level hierarchical headings.
- case title
- /// The font used for second level hierarchical headings.
- case title2
- /// The font used for third level hierarchical headings.
- case title3
- /// The font used for headings.
- case headline
- /// The font used for subheadings.
- case subheadline
- /// The font used for body text.
- case body
- /// The font used for callouts.
- case callout
- /// The font used for standard captions.
- case caption
- /// The font used for alternate captions.
- case caption2
- /// The font used in footnotes.
- case footnote
- }
- }
- extension Font.TextStyle {
- /// A text style's resolved properties.
- public struct Resolved: Sendable {
- /// The point size.
- public var pointSize: Double
- /// The weight.
- public var weight: Font.Weight = .regular
- /// The emphasized weight.
- public var emphasizedWeight: Font.Weight
- /// The line height, in points.
- public var lineHeight: Double
- /// Fallback to macOS's body text style.
- ///
- /// This isn't expected to ever get used because it's only reachable if
- /// we forgot to supply text styles for a new device class or missed a
- /// text style in a device class' text style lookup table.
- static let fallback = Self(
- pointSize: 13,
- weight: .regular,
- emphasizedWeight: .semibold,
- lineHeight: 16
- )
- /// Creates a resolved text style.
- public init(
- pointSize: Double,
- weight: Font.Weight = .regular,
- emphasizedWeight: Font.Weight,
- lineHeight: Double
- ) {
- self.pointSize = pointSize
- self.weight = weight
- self.emphasizedWeight = emphasizedWeight
- self.lineHeight = lineHeight
- }
- }
- /// Resolves the text style's concrete text properties for the given device
- /// class.
- ///
- /// Generally follows [Apple's typography guidelines][typography]. Our
- /// styles only differ from Apple's where Apple decided not to specify a
- /// text style for a specific platform.
- ///
- /// [typography]: https://developer.apple.com/design/human-interface-guidelines/typography
- public func resolve(for deviceClass: DeviceClass) -> Resolved {
- guard let textStyles = Self.resolvedTextStyles[deviceClass] else {
- logger.warning(Logger.Message(stringLiteral: "missing text styles for device class \(deviceClass)"))
- return .fallback
- }
- guard let textStyle = textStyles[self] else {
- logger.warning(Logger.Message(stringLiteral: "missing \(self) text style for device class \(deviceClass)"))
- return .fallback
- }
- return textStyle
- }
- private static let resolvedTextStyles: [DeviceClass: [Self: Resolved]] = [
- .desktop: desktopTextStyles,
- .phone: mobileTextStyles,
- .tablet: mobileTextStyles,
- .tv: tvTextStyles,
- .watch: watchTextStyles,
- ]
- private static let desktopTextStyles: [Self: Resolved] = [
- .largeTitle: Resolved(
- pointSize: 26,
- emphasizedWeight: .bold,
- lineHeight: 32
- ),
- .title: Resolved(
- pointSize: 22,
- emphasizedWeight: .bold,
- lineHeight: 26
- ),
- .title2: Resolved(
- pointSize: 17,
- emphasizedWeight: .bold,
- lineHeight: 22
- ),
- .title3: Resolved(
- pointSize: 15,
- emphasizedWeight: .semibold,
- lineHeight: 20
- ),
- .headline: Resolved(
- pointSize: 13,
- weight: .bold,
- emphasizedWeight: .heavy,
- lineHeight: 16
- ),
- .body: Resolved(
- pointSize: 13,
- emphasizedWeight: .semibold,
- lineHeight: 16
- ),
- .callout: Resolved(
- pointSize: 12,
- emphasizedWeight: .semibold,
- lineHeight: 15
- ),
- .subheadline: Resolved(
- pointSize: 11,
- emphasizedWeight: .semibold,
- lineHeight: 14
- ),
- .footnote: Resolved(
- pointSize: 10,
- emphasizedWeight: .semibold,
- lineHeight: 13
- ),
- .caption: Resolved(
- pointSize: 10,
- emphasizedWeight: .medium,
- lineHeight: 13
- ),
- .caption2: Resolved(
- pointSize: 10,
- weight: .medium,
- emphasizedWeight: .semibold,
- lineHeight: 13
- ),
- ]
- private static let mobileTextStyles: [Self: Resolved] = [
- .largeTitle: Resolved(
- pointSize: 34,
- emphasizedWeight: .semibold,
- lineHeight: 41
- ),
- .title: Resolved(
- pointSize: 28,
- emphasizedWeight: .semibold,
- lineHeight: 34
- ),
- .title2: Resolved(
- pointSize: 22,
- emphasizedWeight: .semibold,
- lineHeight: 28
- ),
- .title3: Resolved(
- pointSize: 20,
- emphasizedWeight: .semibold,
- lineHeight: 25
- ),
- .headline: Resolved(
- pointSize: 17,
- weight: .semibold,
- emphasizedWeight: .semibold,
- lineHeight: 22
- ),
- .body: Resolved(
- pointSize: 17,
- emphasizedWeight: .semibold,
- lineHeight: 22
- ),
- .callout: Resolved(
- pointSize: 16,
- emphasizedWeight: .semibold,
- lineHeight: 21
- ),
- .subheadline: Resolved(
- pointSize: 15,
- emphasizedWeight: .semibold,
- lineHeight: 20
- ),
- .footnote: Resolved(
- pointSize: 13,
- emphasizedWeight: .semibold,
- lineHeight: 18
- ),
- .caption: Resolved(
- pointSize: 12,
- emphasizedWeight: .semibold,
- lineHeight: 16
- ),
- .caption2: Resolved(
- pointSize: 11,
- emphasizedWeight: .semibold,
- lineHeight: 13
- ),
- ]
- // The tvOS large title and footnote styles are the only ones not from the
- // Apple typography guidelines. I've just made it up based off the ratios
- // used on other platforms to provide a consistent set of text styles
- // across all platforms.
- private static let tvTextStyles: [Self: Resolved] = [
- .largeTitle: Resolved(
- pointSize: 91,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 80
- ),
- .title: Resolved(
- pointSize: 76,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 96
- ),
- .title2: Resolved(
- pointSize: 57,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 66
- ),
- .title3: Resolved(
- pointSize: 48,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 56
- ),
- .headline: Resolved(
- pointSize: 38,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 46
- ),
- .body: Resolved(
- pointSize: 29,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 36
- ),
- .callout: Resolved(
- pointSize: 31,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 38
- ),
- .subheadline: Resolved(
- pointSize: 38,
- weight: .regular,
- emphasizedWeight: .medium,
- lineHeight: 46
- ),
- .footnote: Resolved(
- pointSize: 27,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 33
- ),
- .caption: Resolved(
- pointSize: 25,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 32
- ),
- .caption2: Resolved(
- pointSize: 23,
- weight: .medium,
- emphasizedWeight: .bold,
- lineHeight: 30
- ),
- ]
- // Like largeTitle and footnote on TV, callout and subheadline here are inferred from other
- // device classes.
- private static let watchTextStyles: [Self: Resolved] = [
- .largeTitle: Resolved(
- pointSize: 36,
- emphasizedWeight: .bold,
- lineHeight: 38.5
- ),
- .title: Resolved(
- pointSize: 34,
- emphasizedWeight: .semibold,
- lineHeight: 36.5
- ),
- .title2: Resolved(
- pointSize: 28,
- emphasizedWeight: .semibold,
- lineHeight: 30.5
- ),
- .title3: Resolved(
- pointSize: 19,
- emphasizedWeight: .semibold,
- lineHeight: 21.5
- ),
- .headline: Resolved(
- pointSize: 21,
- weight: .semibold,
- emphasizedWeight: .semibold,
- lineHeight: 23.5
- ),
- .body: Resolved(
- pointSize: 21,
- emphasizedWeight: .semibold,
- lineHeight: 23.5
- ),
- .callout: Resolved(
- pointSize: 20,
- emphasizedWeight: .semibold,
- lineHeight: 22.5
- ),
- .subheadline: Resolved(
- pointSize: 19,
- emphasizedWeight: .semibold,
- lineHeight: 21.5
- ),
- .caption: Resolved(
- pointSize: 18,
- emphasizedWeight: .semibold,
- lineHeight: 20.5
- ),
- .caption2: Resolved(
- pointSize: 17,
- emphasizedWeight: .semibold,
- lineHeight: 19.5
- ),
- .footnote: Resolved(
- pointSize: 16,
- emphasizedWeight: .semibold,
- lineHeight: 18.5
- ),
- ]
- }
|