| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- /// A font that can dynamically adapt to the environment.
- public struct Font: Hashable, Sendable {
- /// Gets a system font to use with the specified size, weight, and design.
- public static func system(
- size: Double,
- weight: Weight? = nil,
- design: Design? = nil
- ) -> Font {
- let kind = Kind.concrete(
- identifier: .system,
- size: size,
- weight: weight,
- design: design
- )
- return Font(kind: kind)
- }
- /// Gets a system font that uses the specified style, weight, and design.
- public static func system(
- _ style: Font.TextStyle,
- weight: Weight? = nil,
- design: Design? = nil
- ) -> Font {
- return Font(kind: .dynamic(style))
- .weight(weight)
- .design(design)
- }
- /// The font style for large titles.
- public static let largeTitle = Font(dynamic: .largeTitle)
- /// The font used for first level hierarchical headings.
- public static let title = Font(dynamic: .title)
- /// The font used for second level hierarchical headings.
- public static let title2 = Font(dynamic: .title2)
- /// The font used for third level hierarchical headings.
- public static let title3 = Font(dynamic: .title3)
- /// The font used for headings.
- public static let headline = Font(dynamic: .headline)
- /// The font used for subheadings.
- public static let subheadline = Font(dynamic: .subheadline)
- /// The font used for body text.
- public static let body = Font(dynamic: .body)
- /// The font used for callouts.
- public static let callout = Font(dynamic: .callout)
- /// The font used for standard captions.
- public static let caption = Font(dynamic: .caption)
- /// The font used for alternate captions.
- public static let caption2 = Font(dynamic: .caption2)
- /// The font used in footnotes.
- public static let footnote = Font(dynamic: .footnote)
- /// Selects whether or not to use the font's emphasized variant.
- ///
- /// - Parameter emphasized: Whether to emphasize the font.
- /// - Returns: The updated font.
- public func emphasized(_ emphasized: Bool = true) -> Font {
- var font = self
- font.overlay.emphasize = emphasized
- return font
- }
- /// Selects whether or not to italicize the font.
- ///
- /// - Parameter italic: Whether to italicize the font.
- /// - Returns: The updated font.
- public func italic(_ italic: Bool = true) -> Font {
- var font = self
- font.overlay.italicize = italic
- return font
- }
- /// Overrides the font's weight.
- ///
- /// - Parameter weight: The font's new weight. If `nil`, this method does
- /// nothing.
- /// - Returns: The updated font.
- public func weight(_ weight: Weight?) -> Font {
- var font = self
- if let weight {
- font.overlay.weight = weight
- }
- return font
- }
- /// Overrides the font's design.
- ///
- /// - Parameter design: The font's new design. If `nil`, this method does
- /// nothing.
- /// - Returns: The updated font.
- public func design(_ design: Design?) -> Font {
- var font = self
- if let design {
- font.overlay.design = design
- }
- return font
- }
- /// Overrides the font's point size.
- ///
- /// - Parameter pointSize: The font's new point size.
- /// - Returns: The updated font.
- public func pointSize(_ pointSize: Double) -> Font {
- var font = self
- font.overlay.pointSize = pointSize
- font.overlay.pointSizeScaleFactor = 1
- return font
- }
- /// Scales the font's point size and line height by a given factor.
- ///
- /// - Parameter factor: The factor to scale the point size and line height
- /// by.
- /// - Returns: The updated font.
- public func scaled(by factor: Double) -> Font {
- var font = self
- font.overlay.pointSizeScaleFactor *= factor
- font.overlay.lineHeightScaleFactor *= factor
- return font
- }
- /// Selects whether or not to use the font's monospaced variant.
- ///
- /// - Parameter monospaced: Whether to use the font's monospaced variant.
- /// If `false` and the font is currently monospaced, then the font's
- /// design gets reverted to its default value.
- /// - Returns: The updated font.
- public func monospaced(_ monospaced: Bool = true) -> Font {
- var font = self
- if monospaced {
- font.overlay.design = .monospaced
- } else if font.overlay.design == .monospaced {
- font.overlay.design = .default
- }
- return font
- }
- private var kind: Kind
- private var overlay = Overlay()
- private init(kind: Kind) {
- self.kind = kind
- }
- private init(dynamic textStyle: TextStyle) {
- self.kind = .dynamic(textStyle)
- }
- /// Internal storage enum to hide away Font's implementation.
- private enum Kind: Hashable, Sendable {
- case concrete(
- identifier: Resolved.Identifier,
- size: Double,
- weight: Weight? = nil,
- design: Design? = nil
- )
- case dynamic(TextStyle)
- }
- /// A font weight.
- ///
- /// The cases are in order of increasing weight.
- public enum Weight: Hashable, Sendable, CaseIterable, Codable {
- /// The ultra-light weight.
- case ultraLight
- /// The thin weight.
- case thin
- /// The light weight.
- case light
- /// The regular weight.
- case regular
- /// The medium weight.
- case medium
- /// The semibold weight.
- case semibold
- /// The bold weight.
- case bold
- /// The heavy weight.
- case heavy
- /// The black weight.
- case black
- }
- /// A font's design.
- public enum Design: Hashable, Sendable, CaseIterable, Codable {
- /// The default design.
- case `default`
- /// The monospaced design.
- case monospaced
- }
- /// An overlay applied to a font after resolving its concrete properties.
- struct Overlay: Hashable, Sendable {
- /// Overrides the font's base size. Applied before scaling.
- var pointSize: Double?
- /// Overrides the font's line height. Applied before scaling.
- var lineHeight: Double?
- /// Applied to the font's point size (after applying the ``pointSize``
- /// overlay if present).
- var pointSizeScaleFactor: Double = 1
- /// Applied to the font's line height (after applying the ``lineHeight``
- /// overlay if present).
- var lineHeightScaleFactor: Double = 1
- /// Overrides the font's weight. Applied before (i.e. overridden by)
- /// ``emphasize``.
- var weight: Weight?
- /// If `true`, overrides the font's weight with the font's emphasized
- /// weight. If `false`, does nothing. Applied after the ``weight``
- /// overlay has been applied if one is present.
- var emphasize: Bool = false
- /// If `true`, overrides the font to be italicized. If `false`, does
- /// nothing.
- var italicize: Bool = false
- /// Overrides the font's design.
- var design: Design?
- /// Applies an overlay to a resolved font.
- ///
- /// - Parameters:
- /// - resolvedFont: The font to apply the overlay to. Passed as
- /// `inout`.
- /// - emphasizedWeight: The weight to use for the font's emphasized
- /// variant.
- func apply(
- to resolvedFont: inout Font.Resolved,
- emphasizedWeight: Weight
- ) {
- if let weight {
- resolvedFont.weight = weight
- }
- if let design {
- resolvedFont.design = design
- }
- if emphasize {
- resolvedFont.weight = emphasizedWeight
- }
- if italicize {
- resolvedFont.isItalic = true
- }
- if let pointSize {
- resolvedFont.pointSize = pointSize
- }
- if let lineHeight {
- resolvedFont.lineHeight = lineHeight
- }
- resolvedFont.pointSize *= pointSizeScaleFactor
- resolvedFont.lineHeight *= lineHeightScaleFactor
- }
- }
- /// A resolved font.
- public struct Resolved: Hashable, Sendable {
- /// A font identifier.
- public struct Identifier: Hashable, Sendable {
- @_spi(Backends) public var kind: Kind
- /// The system font.
- public static let system = Self(kind: .system)
- @_spi(Backends) public enum Kind: Hashable, Sendable {
- case system
- }
- }
- /// The font's identifier.
- public var identifier: Identifier
- /// The font's point size.
- public var pointSize: Double
- /// The font's line height, in points.
- public var lineHeight: Double
- /// The font's weight.
- public var weight: Weight
- /// The font's design.
- public var design: Design
- /// Whether the font is italicized.
- public var isItalic: Bool
- }
- public struct Context: Sendable {
- var overlay: Font.Overlay
- var deviceClass: DeviceClass
- var resolveTextStyle: @MainActor @Sendable (TextStyle) -> TextStyle.Resolved
- }
- @MainActor
- @_spi(Backends) public func resolve(in context: Context) -> Resolved {
- let emphasizedWeight: Weight
- var resolved: Resolved
- switch kind {
- case .concrete(let identifier, let size, let weight, let design):
- switch identifier.kind {
- case .system:
- emphasizedWeight = .bold
- resolved = Resolved(
- identifier: .system,
- pointSize: size,
- // TODO: Research which line height ratio would be
- // the best default (or any alternatives to a
- // constant ratio).
- lineHeight: (size * 1.25).rounded(.awayFromZero),
- weight: weight ?? .regular,
- design: design ?? .default,
- isItalic: false
- )
- }
- case .dynamic(let textStyle):
- let resolvedTextStyle = context.resolveTextStyle(textStyle)
- emphasizedWeight = resolvedTextStyle.emphasizedWeight
- resolved = Resolved(
- identifier: .system,
- pointSize: resolvedTextStyle.pointSize,
- lineHeight: resolvedTextStyle.lineHeight,
- weight: resolvedTextStyle.weight,
- design: .default,
- isItalic: false
- )
- }
- overlay.apply(to: &resolved, emphasizedWeight: emphasizedWeight)
- context.overlay.apply(to: &resolved, emphasizedWeight: emphasizedWeight)
- return resolved
- }
- }
|