import SwiftUI /// The type ramp, which is the desktop client's type ramp, under Material's /// names so a view ported from the Android client asks for the same style. /// /// Inter, Regular and SemiBold, as on the desktop and on Android: a `Medium` /// role resolves to Regular. The rungs, largest first: /// 34 / 26 / 20 / 18 / 17 / 15 / 14 / 13 / 12 / 11. The top one is the hero /// line — the banner's name and the title page's — in Inter Display Bold and /// set tighter. `TypeRampTests` pins them. enum TypeRole: CaseIterable { case displayLarge, displayMedium, displaySmall case headlineLarge, headlineMedium, headlineSmall case titleLarge, titleMedium, titleSmall case bodyLarge, bodyMedium, bodySmall case labelLarge, labelMedium, labelSmall var size: CGFloat { switch self { case .displayLarge: Ramp.hero case .displayMedium, .headlineLarge, .headlineMedium: Ramp.display case .displaySmall, .titleLarge: Ramp.title case .headlineSmall: Ramp.heading case .titleMedium: Ramp.section case .titleSmall: Ramp.subhead case .bodyLarge: Ramp.body case .bodyMedium, .labelLarge: Ramp.label case .bodySmall, .labelMedium: Ramp.caption case .labelSmall: Ramp.micro } } var semibold: Bool { switch self { case .displayMedium, .displaySmall, .headlineLarge, .headlineMedium, .headlineSmall, .titleLarge: true default: false } } /// The Dynamic Type style each rung scales with. var relative: Font.TextStyle { switch size { case Ramp.hero: .largeTitle case Ramp.display: .title case Ramp.title: .title2 case Ramp.heading: .title3 case Ramp.section: .headline case Ramp.subhead: .subheadline case Ramp.body: .body case Ramp.label: .callout case Ramp.caption: .footnote default: .caption } } /// Line height is derived rather than named per style: 1.35 of the size, /// as on Android. Inter's own leading is about 1.21, so the rest is spacing. var lineSpacing: CGFloat { size * ((self == .displayLarge ? Ramp.heroLineHeight : Ramp.lineHeight) - 1.21) } /// The PostScript name of the face this style is set in. func face(semibold: Bool? = nil) -> String { if self == .displayLarge, semibold == nil { return "InterDisplay-Bold" } return (semibold ?? self.semibold) ? "Inter-SemiBold" : "Inter-Regular" } } enum Ramp { static let hero: CGFloat = 34 static let display: CGFloat = 26 static let title: CGFloat = 20 static let heading: CGFloat = 18 static let section: CGFloat = 17 static let subhead: CGFloat = 15 static let body: CGFloat = 14 static let label: CGFloat = 13 static let caption: CGFloat = 12 static let micro: CGFloat = 11 static let lineHeight: CGFloat = 1.35 static let heroLineHeight: CGFloat = 1.1 } extension Font { static func inter(_ style: TypeRole, semibold: Bool? = nil) -> Font { .custom(style.face(semibold: semibold), size: style.size, relativeTo: style.relative) } static func inter(size: CGFloat, semibold: Bool = false) -> Font { .custom(semibold ? "Inter-SemiBold" : "Inter-Regular", size: size) } } extension View { /// One rung of the ramp. `semibold` overrides the style's own weight, the /// counterpart of Compose's `fontWeight = FontWeight.SemiBold`. func textStyle(_ style: TypeRole, semibold: Bool? = nil) -> some View { font(.inter(style, semibold: semibold)).lineSpacing(style.lineSpacing) } }