diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index c6484cd63..5438dcfb9 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -137,27 +137,27 @@ extension Text { // MARK: - Text.Style [WIP] package struct Style { - private var baseFont: TextStyleFont - private var fontModifiers: [AnyFontModifier] - private var color: TextStyleColor - private var backgroundColor: Color? - private var baselineOffset: CGFloat? - private var kerning: CGFloat? - private var tracking: CGFloat? - private var strikethrough: LineStyle - private var underline: LineStyle -// private var encapsulation: Text.Encapsulation? - private var speech: AccessibilitySpeechAttributes? + internal var baseFont: TextStyleFont + internal var fontModifiers: [AnyFontModifier] + internal var color: TextStyleColor + internal var backgroundColor: Color? + internal var baselineOffset: CGFloat? + internal var kerning: CGFloat? + internal var tracking: CGFloat? + internal var strikethrough: LineStyle + internal var underline: LineStyle + internal var encapsulation: Text.Encapsulation? + internal var speech: AccessibilitySpeechAttributes? package var accessibility: AccessibilityTextAttributes? -// private var glyphInfo: CTGlyphInfo? -// private var shadow: TextShadowModifier? -// private var transition: TextTransitionModifier? -// private var scale: Text.Scale? -// private var superscript: Text.Superscript? -// private var typesettingConfiguration: TypesettingConfiguration -// private var customAttributes: [TextAttributeModifierBase] +// internal var glyphInfo: CTGlyphInfo? +// internal var shadow: TextShadowModifier? +// internal var transition: TextTransitionModifier? +// internal var scale: Text.Scale? +// internal var superscript: Text.Superscript? + internal var typesettingConfiguration: TypesettingConfiguration +// internal var customAttributes: [TextAttributeModifierBase] #if canImport(Darwin) -// private var adaptiveImageGlyph: AttributedString.AdaptiveImageGlyph? +// internal var adaptiveImageGlyph: AttributedString.AdaptiveImageGlyph? #endif package var clearedFontModifiers: Set diff --git a/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingConfiguration.swift b/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingConfiguration.swift new file mode 100644 index 000000000..30e818169 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingConfiguration.swift @@ -0,0 +1,33 @@ +// +// TypesettingConfiguration.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - TypesettingConfiguration + +package struct TypesettingConfiguration: Equatable { + package var language: TypesettingLanguage + + package var languageAwareLineHeightRatio: TypesettingLanguageAwareLineHeightRatio + + package init( + language: TypesettingLanguage = .automatic, + languageAwareLineHeightRatio: TypesettingLanguageAwareLineHeightRatio = .automatic + ) { + self.language = language + self.languageAwareLineHeightRatio = languageAwareLineHeightRatio + } +} + +package struct TypesettingConfigurationKey: EnvironmentKey { + package static let defaultValue: TypesettingConfiguration = .init() +} + +extension EnvironmentValues { + package var typesettingConfiguration: TypesettingConfiguration { + get { self[TypesettingConfigurationKey.self] } + set { self[TypesettingConfigurationKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingLanguage.swift b/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingLanguage.swift new file mode 100644 index 000000000..c43e6c574 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingLanguage.swift @@ -0,0 +1,211 @@ +// +// TypesettingLanguage.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +public import Foundation + +// MARK: - TypesettingLanguage + +/// Defines how typesetting language is determined for text. +/// +/// Use a modifier like ``View/typesettingLanguage(_:isEnabled:)`` +/// to specify the typesetting language. +@available(OpenSwiftUI_v5_0, *) +public struct TypesettingLanguage: Sendable, Equatable { + package struct Flags: OptionSet { + package var rawValue: UInt8 + + package init(rawValue: UInt8) { + self.rawValue = rawValue + } + + package static let modifyFont: TypesettingLanguage.Flags = .init(rawValue: 1 << 0) + } + + package enum Storage: Equatable { + case automatic + case contentAware + case explicit(Locale.Language, TypesettingLanguage.Flags) + } + + package var storage: TypesettingLanguage.Storage + + /// Automatic language behavior. + /// + /// When determining the language to use for typesetting the current UI + /// language and preferred languages will be considered. For example, if + /// the current UI locale is for English and Thai is included in the + /// preferred languages then line heights will be taller to accommodate the + /// taller glyphs used by Thai. + public static let automatic: TypesettingLanguage = .init(storage: .automatic) + + /// Use explicit language. + /// + /// An explicit language will be used for typesetting. For example, if used + /// with Thai language the line heights will be as tall as needed to + /// accommodate Thai. + /// + /// - Parameters: + /// - language: The language to use for typesetting. + /// - Returns: A `TypesettingLanguage`. + public static func explicit(_ language: Locale.Language) -> TypesettingLanguage { + .init(storage: .explicit(language, .modifyFont)) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension TypesettingLanguage { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public static let contentAware: TypesettingLanguage = .init(storage: .contentAware) +} + +// MARK: - View + typesettingLanguage + +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Specifies the language for typesetting. + /// + /// In some cases `Text` may contain text of a particular language which + /// doesn't match the device UI language. In that case it's useful to + /// specify a language so line height, line breaking and spacing will + /// respect the script used for that language. For example: + /// + /// Text(verbatim: "แอปเปิล") + /// .typesettingLanguage(.init(languageCode: .thai)) + /// + /// Note: this language does not affect text localization. + /// + /// - Parameters: + /// - language: The explicit language to use for typesetting. + /// - isEnabled: A Boolean value that indicates whether text language is + /// added + /// - Returns: A view with the typesetting language set to the value you + /// supply. + @available(OpenSwiftUI_v5_0, *) + nonisolated public func typesettingLanguage( + _ language: Locale.Language, + isEnabled: Bool = true + ) -> some View { + typesettingLanguage(.explicit(language), isEnabled: isEnabled) + } + + /// Specifies the language for typesetting. + /// + /// In some cases `Text` may contain text of a particular language which + /// doesn't match the device UI language. In that case it's useful to + /// specify a language so line height, line breaking and spacing will + /// respect the script used for that language. For example: + /// + /// Text(verbatim: "แอปเปิล").typesettingLanguage( + /// .explicit(.init(languageCode: .thai))) + /// + /// Note: this language does not affect text localized localization. + /// + /// - Parameters: + /// - language: The language to use for typesetting. + /// - isEnabled: A Boolean value that indicates whether text language is + /// added + /// - Returns: A view with the typesetting language set to the value you + /// supply. + @available(OpenSwiftUI_v5_0, *) + nonisolated public func typesettingLanguage( + _ language: TypesettingLanguage, + isEnabled: Bool = true + ) -> some View { + transformEnvironment(\.typesettingConfiguration) { + if isEnabled { + $0.language = language + } + } + } +} + +// MARK: - LanguageTextModifier + +class LanguageTextModifier: AnyTextModifier { + let language: TypesettingLanguage + + init(language: TypesettingLanguage) { + self.language = language + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + // NOTE: This also set languageAwareLineHeightRatio to automatic + style.typesettingConfiguration = .init(language: language) + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? LanguageTextModifier else { + return false + } + return language == other.language + } +} + +// MARK: - Text + typesettingLanguage + +@available(OpenSwiftUI_v1_0, *) +extension Text { + + /// Specifies the language for typesetting. + /// + /// In some cases `Text` may contain text of a particular language which + /// doesn't match the device UI language. In that case it's useful to + /// specify a language so line height, line breaking and spacing will + /// respect the script used for that language. For example: + /// + /// Text(verbatim: "แอปเปิล") + /// .typesettingLanguage(.init(languageCode: .thai)) + /// + /// Note: this language does not affect text localization. + /// + /// - Parameters: + /// - language: The explicit language to use for typesetting. + /// - isEnabled: A Boolean value that indicates whether text language is + /// added + /// - Returns: Text with the typesetting language set to the value you + /// supply. + @available(OpenSwiftUI_v5_0, *) + public func typesettingLanguage( + _ language: Locale.Language, + isEnabled: Bool = true + ) -> Text { + typesettingLanguage(.explicit(language), isEnabled: isEnabled) + } + + /// Specifies the language for typesetting. + /// + /// In some cases `Text` may contain text of a particular language which + /// doesn't match the device UI language. In that case it's useful to + /// specify a language so line height, line breaking and spacing will + /// respect the script used for that language. For example: + /// + /// Text(verbatim: "แอปเปิล").typesettingLanguage( + /// .explicit(.init(languageCode: .thai))) + /// + /// Note: this language does not affect text localized localization. + /// + /// - Parameters: + /// - language: The language to use for typesetting. + /// - isEnabled: A Boolean value that indicates whether text language is + /// added + /// - Returns: Text with the typesetting language set to the value you + /// supply. + @available(OpenSwiftUI_v5_0, *) + public func typesettingLanguage( + _ language: TypesettingLanguage, + isEnabled: Bool = true + ) -> Text { + guard isEnabled else { + return self + } + let modifier: Text.Modifier = .anyTextModifier(LanguageTextModifier(language: language)) + return modified(with: modifier) + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingLanguageAwareLineHeightRatio.swift b/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingLanguageAwareLineHeightRatio.swift new file mode 100644 index 000000000..0ebef8660 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Typesetting/TypesettingLanguageAwareLineHeightRatio.swift @@ -0,0 +1,87 @@ +// +// TypesettingLanguageAwareLineHeightRatio.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - TypesettingLanguageAwareLineHeightRatio + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +public struct TypesettingLanguageAwareLineHeightRatio: Sendable, Equatable { + enum Storage: Equatable { + case custom(Double) + case automatic + case disable + case legacy + } + + var storage: TypesettingLanguageAwareLineHeightRatio.Storage + + public static let automatic: TypesettingLanguageAwareLineHeightRatio = .init(storage: .automatic) + + public static let disable: TypesettingLanguageAwareLineHeightRatio = .init(storage: .disable) + + public static let legacy: TypesettingLanguageAwareLineHeightRatio = .init(storage: .legacy) + + public static func custom(_ ratio: Double) -> TypesettingLanguageAwareLineHeightRatio { + .init(storage: .custom(ratio.clamp(min: 0.0, max: 1.0))) + } +} + +// MARK: - View + typesettingLanguageAwareLineHeightRatio + +@available(OpenSwiftUI_v1_0, *) +extension View { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + nonisolated public func typesettingLanguageAwareLineHeightRatio( + _ ratio: TypesettingLanguageAwareLineHeightRatio, + isEnabled: Bool = true + ) -> some View { + transformEnvironment(\.typesettingConfiguration) { + if isEnabled { + $0.languageAwareLineHeightRatio = ratio + } + } + } +} + +// MARK: - LanguageAwareLineHeightRatioTextModifier + +class LanguageAwareLineHeightRatioTextModifier: AnyTextModifier { + let ratio: TypesettingLanguageAwareLineHeightRatio + + init(ratio: TypesettingLanguageAwareLineHeightRatio) { + self.ratio = ratio + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.typesettingConfiguration.languageAwareLineHeightRatio = ratio + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? LanguageAwareLineHeightRatioTextModifier else { + return false + } + return ratio == other.ratio + } +} + +// MARK: - Text + typesettingLanguageAwareLineHeightRatio + +@available(OpenSwiftUI_v1_0, *) +extension Text { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public func typesettingLanguageAwareLineHeightRatio( + _ ratio: TypesettingLanguageAwareLineHeightRatio, + isEnabled: Bool = true + ) -> Text { + guard isEnabled else { + return self + } + return modified(with: .anyTextModifier(LanguageAwareLineHeightRatioTextModifier(ratio: ratio))) + } +}