mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
162 lines
4.9 KiB
Swift
162 lines
4.9 KiB
Swift
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
/// Manages text attributes.
|
|
final class TextHelper {
|
|
private var cachedAttributes: [AttributesKey: [NSAttributedString.Key: Any]] = [:]
|
|
private var cachedFonts: [TextStyle: UXFont] = [:]
|
|
|
|
init() {}
|
|
|
|
func attributes(
|
|
role: TextRole,
|
|
style: TextFontStyle = .proportional,
|
|
weight: UXFont.Weight = .regular,
|
|
width: TextWidth = .standard,
|
|
color: UXColor? = .label
|
|
) -> [NSAttributedString.Key: Any] {
|
|
attributes(style: .init(role: role, style: style, weight: weight, width: width), color: color)
|
|
}
|
|
|
|
private(set) lazy var spacerAttributes: [NSAttributedString.Key: Any] = [
|
|
.font: scaled(font: UXFont.systemFont(ofSize: 10))
|
|
]
|
|
|
|
func attributes(style: TextStyle, color: UXColor?) -> [NSAttributedString.Key: Any] {
|
|
let key = AttributesKey(textStyle: style, color: color)
|
|
if let attributes = cachedAttributes[key] {
|
|
return attributes
|
|
}
|
|
let attributes = makeAttributes(style: style, color: color)
|
|
cachedAttributes[key] = attributes
|
|
return attributes
|
|
}
|
|
|
|
func font(style: TextStyle) -> UXFont {
|
|
if let font = cachedFonts[style] {
|
|
return font
|
|
}
|
|
let font = makeFont(style: style)
|
|
cachedFonts[style] = font
|
|
return font
|
|
}
|
|
|
|
private let titleParagraphStyle: NSParagraphStyle = {
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.lineSpacing = -6
|
|
paragraphStyle.baseWritingDirection = .leftToRight
|
|
return paragraphStyle
|
|
}()
|
|
|
|
private let bodyParagraphStyle: NSParagraphStyle = {
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.lineSpacing = 3
|
|
paragraphStyle.baseWritingDirection = .leftToRight
|
|
return paragraphStyle
|
|
}()
|
|
|
|
private func makeAttributes(style: TextStyle, color: UXColor?) -> [NSAttributedString.Key: Any] {
|
|
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
let font = self.font(style: style)
|
|
attributes[.font] = font
|
|
attributes[.paragraphStyle] = style.role == .title ? titleParagraphStyle : bodyParagraphStyle
|
|
if style.width == .condensed {
|
|
attributes[.kern] = -0.4
|
|
} else if style.style == .monospaced {
|
|
attributes[.kern] = -0.3
|
|
}
|
|
attributes[.foregroundColor] = color
|
|
if style.role == .subheadline {
|
|
attributes[.subheadline] = true
|
|
}
|
|
return attributes
|
|
}
|
|
|
|
private func makeFont(style: TextStyle) -> UXFont {
|
|
var size: CGFloat
|
|
let body2Size = (0.9 * getPreferredFontSize(for: .body)).rounded()
|
|
switch style.role {
|
|
#if os(watchOS)
|
|
case .title: size = getPreferredFontSize(for: .title2)
|
|
#else
|
|
case .title: size = getPreferredFontSize(for: .title1)
|
|
#endif
|
|
case .subheadline:
|
|
#if os(macOS)
|
|
size = (0.9 * body2Size).rounded()
|
|
#else
|
|
size = (0.84 * body2Size).rounded()
|
|
#endif
|
|
case .body: size = getPreferredFontSize(for: .body)
|
|
case .body2: size = body2Size
|
|
}
|
|
#if !os(macOS)
|
|
if style.style == .monospaced { size -= 2 } // Appears larger than regular
|
|
#endif
|
|
return scaled(font: {
|
|
switch style.style {
|
|
case .proportional: return .systemFont(ofSize: size, weight: style.weight)
|
|
case .monospaced: return .monospacedSystemFont(ofSize: size, weight: style.weight)
|
|
case .monospacedDigital: return .monospacedDigitSystemFont(ofSize: size, weight: style.weight)
|
|
}
|
|
}())
|
|
}
|
|
|
|
private func scaled(font: UXFont) -> UXFont {
|
|
#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
|
|
return UIFontMetrics.default.scaledFont(for: font)
|
|
#else
|
|
return font
|
|
#endif
|
|
}
|
|
|
|
private struct AttributesKey: Hashable {
|
|
let textStyle: TextStyle
|
|
let color: UXColor?
|
|
}
|
|
}
|
|
|
|
struct TextStyle: Hashable {
|
|
var role: TextRole
|
|
var style: TextFontStyle = .proportional
|
|
var weight: UXFont.Weight = .regular
|
|
var width: TextWidth = .standard
|
|
}
|
|
|
|
enum TextRole {
|
|
/// Large title.
|
|
case title
|
|
/// Section headline (small).
|
|
///
|
|
/// Font size: iOS 12, macOS 10, tvOS 21, watchOS 11
|
|
case subheadline
|
|
/// Regular-sized body.
|
|
///
|
|
/// Font size: iOS 17, macOS 13, tvOS 29, watchOS 16.
|
|
case body
|
|
/// Smaller body for console and other views where information has to be
|
|
/// condensed.
|
|
///
|
|
/// Font size: iOS 15, macOS 12, tvOS 26, watchOS 14.
|
|
case body2
|
|
}
|
|
|
|
enum TextFontStyle {
|
|
case proportional
|
|
case monospaced
|
|
case monospacedDigital
|
|
}
|
|
|
|
enum TextWidth {
|
|
case condensed
|
|
case standard
|
|
}
|
|
|
|
private func getPreferredFontSize(for style: UXFont.TextStyle) -> CGFloat {
|
|
UXFont.preferredFont(forTextStyle: style).fontDescriptor.pointSize
|
|
}
|