Font.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /// A font that can dynamically adapt to the environment.
  2. public struct Font: Hashable, Sendable {
  3. /// Gets a system font to use with the specified size, weight, and design.
  4. public static func system(
  5. size: Double,
  6. weight: Weight? = nil,
  7. design: Design? = nil
  8. ) -> Font {
  9. let kind = Kind.concrete(
  10. identifier: .system,
  11. size: size,
  12. weight: weight,
  13. design: design
  14. )
  15. return Font(kind: kind)
  16. }
  17. /// Gets a system font that uses the specified style, weight, and design.
  18. public static func system(
  19. _ style: Font.TextStyle,
  20. weight: Weight? = nil,
  21. design: Design? = nil
  22. ) -> Font {
  23. return Font(kind: .dynamic(style))
  24. .weight(weight)
  25. .design(design)
  26. }
  27. /// The font style for large titles.
  28. public static let largeTitle = Font(dynamic: .largeTitle)
  29. /// The font used for first level hierarchical headings.
  30. public static let title = Font(dynamic: .title)
  31. /// The font used for second level hierarchical headings.
  32. public static let title2 = Font(dynamic: .title2)
  33. /// The font used for third level hierarchical headings.
  34. public static let title3 = Font(dynamic: .title3)
  35. /// The font used for headings.
  36. public static let headline = Font(dynamic: .headline)
  37. /// The font used for subheadings.
  38. public static let subheadline = Font(dynamic: .subheadline)
  39. /// The font used for body text.
  40. public static let body = Font(dynamic: .body)
  41. /// The font used for callouts.
  42. public static let callout = Font(dynamic: .callout)
  43. /// The font used for standard captions.
  44. public static let caption = Font(dynamic: .caption)
  45. /// The font used for alternate captions.
  46. public static let caption2 = Font(dynamic: .caption2)
  47. /// The font used in footnotes.
  48. public static let footnote = Font(dynamic: .footnote)
  49. /// Selects whether or not to use the font's emphasized variant.
  50. ///
  51. /// - Parameter emphasized: Whether to emphasize the font.
  52. /// - Returns: The updated font.
  53. public func emphasized(_ emphasized: Bool = true) -> Font {
  54. var font = self
  55. font.overlay.emphasize = emphasized
  56. return font
  57. }
  58. /// Selects whether or not to italicize the font.
  59. ///
  60. /// - Parameter italic: Whether to italicize the font.
  61. /// - Returns: The updated font.
  62. public func italic(_ italic: Bool = true) -> Font {
  63. var font = self
  64. font.overlay.italicize = italic
  65. return font
  66. }
  67. /// Overrides the font's weight.
  68. ///
  69. /// - Parameter weight: The font's new weight. If `nil`, this method does
  70. /// nothing.
  71. /// - Returns: The updated font.
  72. public func weight(_ weight: Weight?) -> Font {
  73. var font = self
  74. if let weight {
  75. font.overlay.weight = weight
  76. }
  77. return font
  78. }
  79. /// Overrides the font's design.
  80. ///
  81. /// - Parameter design: The font's new design. If `nil`, this method does
  82. /// nothing.
  83. /// - Returns: The updated font.
  84. public func design(_ design: Design?) -> Font {
  85. var font = self
  86. if let design {
  87. font.overlay.design = design
  88. }
  89. return font
  90. }
  91. /// Overrides the font's point size.
  92. ///
  93. /// - Parameter pointSize: The font's new point size.
  94. /// - Returns: The updated font.
  95. public func pointSize(_ pointSize: Double) -> Font {
  96. var font = self
  97. font.overlay.pointSize = pointSize
  98. font.overlay.pointSizeScaleFactor = 1
  99. return font
  100. }
  101. /// Scales the font's point size and line height by a given factor.
  102. ///
  103. /// - Parameter factor: The factor to scale the point size and line height
  104. /// by.
  105. /// - Returns: The updated font.
  106. public func scaled(by factor: Double) -> Font {
  107. var font = self
  108. font.overlay.pointSizeScaleFactor *= factor
  109. font.overlay.lineHeightScaleFactor *= factor
  110. return font
  111. }
  112. /// Selects whether or not to use the font's monospaced variant.
  113. ///
  114. /// - Parameter monospaced: Whether to use the font's monospaced variant.
  115. /// If `false` and the font is currently monospaced, then the font's
  116. /// design gets reverted to its default value.
  117. /// - Returns: The updated font.
  118. public func monospaced(_ monospaced: Bool = true) -> Font {
  119. var font = self
  120. if monospaced {
  121. font.overlay.design = .monospaced
  122. } else if font.overlay.design == .monospaced {
  123. font.overlay.design = .default
  124. }
  125. return font
  126. }
  127. private var kind: Kind
  128. private var overlay = Overlay()
  129. private init(kind: Kind) {
  130. self.kind = kind
  131. }
  132. private init(dynamic textStyle: TextStyle) {
  133. self.kind = .dynamic(textStyle)
  134. }
  135. /// Internal storage enum to hide away Font's implementation.
  136. private enum Kind: Hashable, Sendable {
  137. case concrete(
  138. identifier: Resolved.Identifier,
  139. size: Double,
  140. weight: Weight? = nil,
  141. design: Design? = nil
  142. )
  143. case dynamic(TextStyle)
  144. }
  145. /// A font weight.
  146. ///
  147. /// The cases are in order of increasing weight.
  148. public enum Weight: Hashable, Sendable, CaseIterable, Codable {
  149. /// The ultra-light weight.
  150. case ultraLight
  151. /// The thin weight.
  152. case thin
  153. /// The light weight.
  154. case light
  155. /// The regular weight.
  156. case regular
  157. /// The medium weight.
  158. case medium
  159. /// The semibold weight.
  160. case semibold
  161. /// The bold weight.
  162. case bold
  163. /// The heavy weight.
  164. case heavy
  165. /// The black weight.
  166. case black
  167. }
  168. /// A font's design.
  169. public enum Design: Hashable, Sendable, CaseIterable, Codable {
  170. /// The default design.
  171. case `default`
  172. /// The monospaced design.
  173. case monospaced
  174. }
  175. /// An overlay applied to a font after resolving its concrete properties.
  176. struct Overlay: Hashable, Sendable {
  177. /// Overrides the font's base size. Applied before scaling.
  178. var pointSize: Double?
  179. /// Overrides the font's line height. Applied before scaling.
  180. var lineHeight: Double?
  181. /// Applied to the font's point size (after applying the ``pointSize``
  182. /// overlay if present).
  183. var pointSizeScaleFactor: Double = 1
  184. /// Applied to the font's line height (after applying the ``lineHeight``
  185. /// overlay if present).
  186. var lineHeightScaleFactor: Double = 1
  187. /// Overrides the font's weight. Applied before (i.e. overridden by)
  188. /// ``emphasize``.
  189. var weight: Weight?
  190. /// If `true`, overrides the font's weight with the font's emphasized
  191. /// weight. If `false`, does nothing. Applied after the ``weight``
  192. /// overlay has been applied if one is present.
  193. var emphasize: Bool = false
  194. /// If `true`, overrides the font to be italicized. If `false`, does
  195. /// nothing.
  196. var italicize: Bool = false
  197. /// Overrides the font's design.
  198. var design: Design?
  199. /// Applies an overlay to a resolved font.
  200. ///
  201. /// - Parameters:
  202. /// - resolvedFont: The font to apply the overlay to. Passed as
  203. /// `inout`.
  204. /// - emphasizedWeight: The weight to use for the font's emphasized
  205. /// variant.
  206. func apply(
  207. to resolvedFont: inout Font.Resolved,
  208. emphasizedWeight: Weight
  209. ) {
  210. if let weight {
  211. resolvedFont.weight = weight
  212. }
  213. if let design {
  214. resolvedFont.design = design
  215. }
  216. if emphasize {
  217. resolvedFont.weight = emphasizedWeight
  218. }
  219. if italicize {
  220. resolvedFont.isItalic = true
  221. }
  222. if let pointSize {
  223. resolvedFont.pointSize = pointSize
  224. }
  225. if let lineHeight {
  226. resolvedFont.lineHeight = lineHeight
  227. }
  228. resolvedFont.pointSize *= pointSizeScaleFactor
  229. resolvedFont.lineHeight *= lineHeightScaleFactor
  230. }
  231. }
  232. /// A resolved font.
  233. public struct Resolved: Hashable, Sendable {
  234. /// A font identifier.
  235. public struct Identifier: Hashable, Sendable {
  236. @_spi(Backends) public var kind: Kind
  237. /// The system font.
  238. public static let system = Self(kind: .system)
  239. @_spi(Backends) public enum Kind: Hashable, Sendable {
  240. case system
  241. }
  242. }
  243. /// The font's identifier.
  244. public var identifier: Identifier
  245. /// The font's point size.
  246. public var pointSize: Double
  247. /// The font's line height, in points.
  248. public var lineHeight: Double
  249. /// The font's weight.
  250. public var weight: Weight
  251. /// The font's design.
  252. public var design: Design
  253. /// Whether the font is italicized.
  254. public var isItalic: Bool
  255. }
  256. public struct Context: Sendable {
  257. var overlay: Font.Overlay
  258. var deviceClass: DeviceClass
  259. var resolveTextStyle: @MainActor @Sendable (TextStyle) -> TextStyle.Resolved
  260. }
  261. @MainActor
  262. @_spi(Backends) public func resolve(in context: Context) -> Resolved {
  263. let emphasizedWeight: Weight
  264. var resolved: Resolved
  265. switch kind {
  266. case .concrete(let identifier, let size, let weight, let design):
  267. switch identifier.kind {
  268. case .system:
  269. emphasizedWeight = .bold
  270. resolved = Resolved(
  271. identifier: .system,
  272. pointSize: size,
  273. // TODO: Research which line height ratio would be
  274. // the best default (or any alternatives to a
  275. // constant ratio).
  276. lineHeight: (size * 1.25).rounded(.awayFromZero),
  277. weight: weight ?? .regular,
  278. design: design ?? .default,
  279. isItalic: false
  280. )
  281. }
  282. case .dynamic(let textStyle):
  283. let resolvedTextStyle = context.resolveTextStyle(textStyle)
  284. emphasizedWeight = resolvedTextStyle.emphasizedWeight
  285. resolved = Resolved(
  286. identifier: .system,
  287. pointSize: resolvedTextStyle.pointSize,
  288. lineHeight: resolvedTextStyle.lineHeight,
  289. weight: resolvedTextStyle.weight,
  290. design: .default,
  291. isItalic: false
  292. )
  293. }
  294. overlay.apply(to: &resolved, emphasizedWeight: emphasizedWeight)
  295. context.overlay.apply(to: &resolved, emphasizedWeight: emphasizedWeight)
  296. return resolved
  297. }
  298. }