diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift index fb5bb1749..0fb5a37ef 100644 --- a/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift @@ -5,6 +5,8 @@ // Audited for 6.5.4 // Status: Blocked by Text.Style +package import Foundation + // MARK: - AccessibilityAnnouncementPriority @_spi(_) @@ -81,14 +83,14 @@ package struct AccessibilitySpeechAttributes: Equatable { } } -// MARK: - Text.Style + AccessibilitySpeechAttributes [WIP] - -//extension Text.Style { -// package func resolveAccessibilitySpeechAttributes( -// into attributes: inout [NSAttributedString.Key: Any], -// environment: EnvironmentValues, -// includeDefaultAttributes: Bool = true -// ) { -// _openSwiftUIUnimplementedFailure() -// } -//} +// MARK: - Text.Style + AccessibilitySpeechAttributes + +extension Text.Style { + package func resolveAccessibilitySpeechAttributes( + into attributes: inout [NSAttributedString.Key: Any], + environment: EnvironmentValues, + includeDefaultAttributes: Bool = true + ) { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/Animation/Timeline/TimelineSchedule.swift b/Sources/OpenSwiftUICore/Animation/Timeline/TimelineSchedule.swift index 24b02db58..56e343699 100644 --- a/Sources/OpenSwiftUICore/Animation/Timeline/TimelineSchedule.swift +++ b/Sources/OpenSwiftUICore/Animation/Timeline/TimelineSchedule.swift @@ -40,7 +40,7 @@ public protocol TimelineSchedule { /// of dates in ascending order. A ``TimelineView`` that you create with a /// schedule updates its content at the moments in time corresponding to /// the dates included in the sequence. - associatedtype Entries: Sequence where Self.Entries.Element == Date + associatedtype Entries: Sequence where Entries.Element == Date /// Provides a sequence of dates starting around a given date. /// @@ -79,7 +79,7 @@ public protocol TimelineSchedule { /// - mode: An indication of whether the schedule updates normally, /// or with some other cadence. /// - Returns: A sequence of dates in ascending order. - func entries(from startDate: Date, mode: Self.Mode) -> Self.Entries + func entries(from startDate: Date, mode: Mode) -> Entries } // MARK: - TimelineScheduleMode diff --git a/Sources/OpenSwiftUICore/View/Text/Accessibility/NSAttributedString+Accessibility.swift b/Sources/OpenSwiftUICore/View/Text/Accessibility/NSAttributedString+Accessibility.swift index 47cfa2627..9e18a9a35 100644 --- a/Sources/OpenSwiftUICore/View/Text/Accessibility/NSAttributedString+Accessibility.swift +++ b/Sources/OpenSwiftUICore/View/Text/Accessibility/NSAttributedString+Accessibility.swift @@ -11,12 +11,21 @@ package import Foundation // MARK: - Accessibility + Text resolve [WIP] extension AccessibilityCore { - package static func textResolvesToEmpty(_ text: Text, in environment: EnvironmentValues) -> Bool { - _openSwiftUIUnimplementedFailure() + package static func textResolvesToEmpty( + _ text: Text, + in environment: EnvironmentValues + ) -> Bool { + guard let storedAccessibilityLabel = text.storedAccessibilityLabel else { + return text.resolvesToEmpty(in: environment, with: .includeAccessibility) + } + return textResolvesToEmpty(storedAccessibilityLabel, in: environment) } - package static func textsResolveToEmpty(_ texts: [Text], in environment: EnvironmentValues) -> Bool { - _openSwiftUIUnimplementedFailure() + package static func textsResolveToEmpty( + _ texts: [Text], + in environment: EnvironmentValues + ) -> Bool { + texts.allSatisfy { textResolvesToEmpty($0, in: environment) } } package static func textResolvedToPlainText( @@ -25,7 +34,15 @@ extension AccessibilityCore { updateResolvableAttributes: Bool = false, idiom: AnyInterfaceIdiom? = nil ) -> String { - _openSwiftUIUnimplementedFailure() + guard let storedAccessibilityLabel = text.storedAccessibilityLabel else { + _openSwiftUIUnimplementedFailure() + } + return textResolvedToPlainText( + storedAccessibilityLabel, + in: environment, + updateResolvableAttributes: updateResolvableAttributes, + idiom: idiom + ) } package static func textsResolvedToPlainText( diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ConfigurationBasedResolvableStringAttribute.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ConfigurationBasedResolvableStringAttribute.swift new file mode 100644 index 000000000..4f0844a05 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ConfigurationBasedResolvableStringAttribute.swift @@ -0,0 +1,122 @@ +// +// ConfigurationBasedResolvableStringAttribute.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Missing ResolvableAttributeConfiguration implementation +// ID: A318841E6831BFF835E45F725C9F7477 (SwiftUICore) + +package import Foundation + +// MARK: - ConfigurationBasedResolvableStringAttribute + +package protocol ConfigurationBasedResolvableStringAttribute: ConfigurationBasedResolvableStringAttributeRepresentation, ResolvableStringAttribute {} + +// MARK: - ConfigurationBasedResolvableStringAttributeRepresentation + +package protocol ConfigurationBasedResolvableStringAttributeRepresentation: Decodable, Encodable, ResolvableStringAttributeFamily, ResolvableStringAttributeRepresentation { + var invalidationConfiguration: ResolvableAttributeConfiguration { get } +} + +extension ConfigurationBasedResolvableStringAttributeRepresentation { + package var schedule: ResolvableAttributeConfiguration.Schedule? { + ResolvableAttributeConfiguration.Schedule(config: invalidationConfiguration) + } +} + +// MARK: - ResolvableAttributeConfiguration [WIP] + +package enum ResolvableAttributeConfiguration: Equatable { + case none + case interval(delay: Double? = nil) + case timer(end: Date) + case timerInterval(interval: DateInterval, countdown: Bool) + case wallClock(alignment: NSCalendar.Unit) + + package var isDynamic: Bool { + switch self { + case .none: false + case .interval(let delay): delay != nil + case .timer: true + case .timerInterval: true + case .wallClock: true + } + } + + mutating package func reduce(_ other: ResolvableAttributeConfiguration) { + switch (self, other) { + case let (.interval(lhsDelay), .interval(rhsDelay)): + if let lhsDelay, let rhsDelay { + self = .interval(delay: min(lhsDelay, rhsDelay)) + } else { + self = .interval(delay: lhsDelay ?? rhsDelay) + } + case let (.wallClock(alignment: lhsAlignment), .wallClock(alignment: rhsAlignment)): + _openSwiftUIUnimplementedFailure() + // WIP: handle other combinations + default: + break + } + } +} + +extension ResolvableAttributeConfiguration { + package struct Schedule: TimelineSchedule { + enum Alignment { + case interval(period: Double) + case timer(end: Date) + case timerInterval(interval: DateInterval, countdown: Bool) + case wallClock(alignment: NSCalendar.Unit) + } + + var alignment: Alignment + + package init?(config: ResolvableAttributeConfiguration) { + switch config { + case .none: return nil + case .interval(let delay): + guard let delay else { + return nil + } + alignment = .interval(period: delay) + case .timer(let end): + alignment = .timer(end: end) + case .timerInterval(let interval, let countdown): + alignment = .timerInterval(interval: interval, countdown: countdown) + case .wallClock(let alignment): + self.alignment = .wallClock(alignment: alignment) + } + } + + package func entries( + from startDate: Date, + mode: TimelineScheduleMode + ) -> AnySequence { + _openSwiftUIUnimplementedFailure() + } + } +} + +extension ResolvableAttributeConfiguration: Codable { + enum Errors: Error { + case missingValue + } + + private enum CodingKeys: CodingKey { + case interval + case delay + case wallClock + case alignment + case timer + case countdowns + case timeInterval + } + + package func encode(to encoder: any Encoder) throws { + _openSwiftUIUnimplementedFailure() + } + + package init(from decoder: any Decoder) throws { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableAbsoluteDate.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableAbsoluteDate.swift new file mode 100644 index 000000000..b323f6dbe --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableAbsoluteDate.swift @@ -0,0 +1,62 @@ +// +// ResolvableAbsoluteDate.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP + +package import Foundation + +// MARK: - ResolvableAbsoluteDate + +package struct ResolvableAbsoluteDate { + package var date: Date + package let style: Text.DateStyle + package let calendar: Calendar + package let locale: Locale + package let timeZone: TimeZone + + package init( + _ date: Date, + style: Text.DateStyle, + in environment: EnvironmentValues + ) { + self.date = date + self.style = style + self.calendar = environment.calendar + self.locale = environment.locale + self.timeZone = environment.timeZone + } +} + +extension ResolvableAbsoluteDate: ConfigurationBasedResolvableStringAttributeRepresentation { + package static func decode( + from decoder: any Decoder + ) throws -> (any ResolvableStringAttribute)? { + _openSwiftUIUnimplementedFailure() + } + + package static let attribute: NSAttributedString.Key = .init("OpenSwiftUI.ResolvableAbsoluteDate") + + package var invalidationConfiguration: ResolvableAttributeConfiguration { + _openSwiftUIUnimplementedFailure() + } + + package func encode(to encoder: any Encoder) throws { + _openSwiftUIUnimplementedFailure() + } + + package init(from decoder: any Decoder) throws { + _openSwiftUIUnimplementedFailure() + } +} + +extension ResolvableAbsoluteDate: Equatable { + package static func == (a: ResolvableAbsoluteDate, b: ResolvableAbsoluteDate) -> Bool { + a.date == b.date && + a.style == b.style && + a.calendar == b.calendar && + a.locale == b.locale && + a.timeZone == b.timeZone + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift new file mode 100644 index 000000000..de19c5407 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift @@ -0,0 +1,188 @@ +// +// ResolvableStringAttribute.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 6237733B8EBAC19656F21E79CFCF2D67 (SwiftUICore) + +package import Foundation + +// MARK: - ResolvableStringResolutionContext + +package struct ResolvableStringResolutionContext { + package var referenceDate: Date? + package var environment: EnvironmentValues + package var maximumWidth: CGFloat? + + package var date: Date { + referenceDate ?? environment.stringResolutionDate ?? .now + } + + package init( + referenceDate: Date? = nil, + environment: EnvironmentValues, + maximumWidth: CGFloat? = nil + ) { + self.referenceDate = referenceDate + self.environment = environment + self.maximumWidth = maximumWidth + } + + package init( + environment: EnvironmentValues, + maximumWidth: CGFloat? = nil + ) { + self.referenceDate = environment.resolvableStringReferenceDate + self.environment = environment + self.maximumWidth = maximumWidth + } +} + +// MARK: - ResolvableStringAttributeFamily + +package protocol ResolvableStringAttributeFamily { + static var attribute: NSAttributedString.Key { get } + + static func decode( + from decoder: any Decoder + ) throws -> (any ResolvableStringAttribute)? +} + +// MARK: - ResolvableStringAttributeRepresentation + +package protocol ResolvableStringAttributeRepresentation { + associatedtype Family: ResolvableStringAttributeFamily + + static func encode( + _ resolvable: Self, + to encoder: any Encoder + ) throws + + func representation( + for version: ArchivedViewInput.DeploymentVersion + ) -> any ResolvableStringAttributeRepresentation +} + +// MARK: - ResolvableStringAttribute + +package protocol ResolvableStringAttribute: ResolvableStringAttributeRepresentation, TimelineSchedule where Entries == AnySequence { + associatedtype Schedule: TimelineSchedule + + func resolve(in context: ResolvableStringResolutionContext) -> AttributedString? + + var schedule: Schedule? { get } + + var requiredFeatures: Text.ResolvedProperties.Features { get } + + mutating func makePlatformAttributes(resolver: inout PlatformAttributeResolver) + + func sizeVariant(_ sizeVariant: TextSizeVariant) -> (resolvable: Self, exact: Bool) +} + +extension ResolvableStringAttributeRepresentation where Self: ResolvableStringAttributeFamily { + package typealias Family = Self +} + +extension ResolvableStringAttributeRepresentation where Self: Decodable, Self: Encodable, Self: ResolvableStringAttributeFamily { + package static func encode( + _ resolvable: Self, + to encoder: any Encoder + ) throws { + try resolvable.encode(to: encoder) + } +} + +extension ResolvableStringAttribute where Self: Decodable, Self: Encodable, Self: ResolvableStringAttributeFamily { + package static func decode( + from decoder: any Decoder + ) throws -> (any ResolvableStringAttribute)? { + try Self(from: decoder) + } +} + +extension ResolvableStringAttributeRepresentation { + package func representation( + for version: ArchivedViewInput.DeploymentVersion + ) -> any ResolvableStringAttributeRepresentation { + self + } +} + +extension ResolvableStringAttributeRepresentation { + package static var attribute: NSAttributedString.Key { + Family.attribute + } +} + +extension ResolvableStringAttribute { + package var requiredFeatures: Text.ResolvedProperties.Features { + [] + } +} + +extension ResolvableStringAttribute { + package mutating func makePlatformAttributes(resolver: inout PlatformAttributeResolver) { + _openSwiftUIEmptyStub() + } +} + +extension ResolvableStringAttribute { + package func sizeVariant(_ sizeVariant: TextSizeVariant) -> (resolvable: Self, exact: Bool) { + (self, sizeVariant.rawValue == 0) + } +} + +extension ResolvableStringAttribute { + package var isDynamic: Bool { + schedule != nil + } + + package func entries( + from startDate: Date, + mode: TimelineScheduleMode + ) -> AnySequence { + guard let schedule else { return AnySequence([]) } + return AnySequence(schedule.entries(from: startDate, mode: mode)) + } +} + +extension EnvironmentValues { + private struct ResolvableStringReferenceDateKey: EnvironmentKey { + static let defaultValue: Date? = nil + } + + package var resolvableStringReferenceDate: Date? { + get { self[ResolvableStringReferenceDateKey.self] } + set { self[ResolvableStringReferenceDateKey.self] = newValue } + } +} + +extension EnvironmentValues { + private struct StringResolutionDate: EnvironmentKey { + static let defaultValue: Date? = nil + } + + package var stringResolutionDate: Date? { + get { self[StringResolutionDate.self] } + set { self[StringResolutionDate.self] = newValue } + } +} + +// FIXME: PlatformAttributeResolver + +package struct PlatformAttributeResolver { + let content: String + let style: Text.Style + let environment: EnvironmentValues + let options: Text.ResolveOptions + let defaultAttributes: [NSAttributedString.Key: Any] + var properties: Text.ResolvedProperties + + func platformAttributes( + for container: AttributeContainer, + includeDefaultValueAttributes: Bool + ) -> [NSAttributedString.Key: Any] { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift similarity index 56% rename from Sources/OpenSwiftUICore/View/Text/Text/ResolvedText.swift rename to Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 068e732ec..c6484cd63 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -134,8 +134,170 @@ extension Text { // } } - // TODO - package struct Style {} + // 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? + 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] + #if canImport(Darwin) +// private var adaptiveImageGlyph: AttributedString.AdaptiveImageGlyph? + #endif + package var clearedFontModifiers: Set + + init() { + _openSwiftUIUnimplementedFailure() + } + + // MARK: - Text.Style.LineStyle + + package enum LineStyle { + case implicit + case explicit(Text.LineStyle) + case `default` + + package func resolve( + in environment: EnvironmentValues, + fallbackStyle: @autoclosure () -> Text.LineStyle? + ) -> Text.LineStyle.Resolved? { + let style: Text.LineStyle + switch self { + case .implicit: + guard let fallbackStyle = fallbackStyle() else { + return nil + } + style = fallbackStyle + case let .explicit(lineStyle): + style = lineStyle + case .default: + return nil + } + return Text.LineStyle.Resolved( + nsUnderlineStyle: style.nsUnderlineStyle, + color: style.color?.resolve(in: environment) + ) + } + } + + // MARK: - Text.Style.TextStyleColor [WIP] + + package enum TextStyleColor { + case implicit + case explicit(AnyShapeStyle) + case `default` + case foregroundKeyColor(base: AnyShapeStyle) + + package func resolve( + in environment: EnvironmentValues, + with options: Text.ResolveOptions, + properties: inout Text.ResolvedProperties, + includeDefaultAttributes: Bool = true + ) -> Color.Resolved? { + let style: AnyShapeStyle + switch self { + case .implicit: + guard includeDefaultAttributes else { + return nil + } + guard !options.contains(.foregroundKeyColor) else { + return .init(linearWhite: -1, opacity: 1) + } + let s = environment.defaultForegroundStyle + style = .init(s?.fallbackColor(in: environment, level: 0) ?? .primary) + case .explicit(let anyShapeStyle): + style = anyShapeStyle + case .default: + guard includeDefaultAttributes else { + return nil + } + guard !options.contains(.foregroundKeyColor) else { + return .init(linearWhite: -1, opacity: 1) + } + let s = environment.foregroundStyle + style = .init(s?.fallbackColor(in: environment, level: 0) ?? .primary) + case .foregroundKeyColor(let base): + guard !options.contains(.allowsKeyColors) else { + return .init(linearWhite: -1, opacity: 1) + } + style = base + } + if options.contains(.allowsKeyColors) { + var shape = _ShapeStyle_Shape( + operation: .resolveStyle(name: .foreground, levels: 0..<1), + environment: environment, + role: .stroke + ) + style._apply(to: &shape) + let shapeStyle = shape.stylePack[.foreground, 0] + return properties.addCustomStyle(shapeStyle) + } else { + let color = style.fallbackColor(in: environment, level: 0) ?? .foreground + return color.resolve(in: environment) + } + } + } + + // MARK: - Text.Style.TextStyleFont + + package enum TextStyleFont { + case implicit + case explicit(Font) + case `default` + + package func resolve( + in environment: EnvironmentValues, + includeDefaultAttributes: Bool = true + ) -> Font? { + guard case let .explicit(font) = self else { + guard includeDefaultAttributes else { + return nil + } + if case .implicit = self { + return environment.effectiveFont + } else { // default + return environment.defaultFont ?? environment.fallbackFont + } + } + return font + } + } + + package func fontTraits(in environment: EnvironmentValues) -> Font.ResolvedTraits { + _openSwiftUIUnimplementedFailure() + } + + package mutating func addFontModifier(_ modifier: M) where M: FontModifier { + _openSwiftUIUnimplementedFailure() + } + + package mutating func addFontModifier(type: M.Type) where M: StaticFontModifier { + _openSwiftUIUnimplementedFailure() + } + + package mutating func removeFontModifier(ofType _: M.Type) where M: FontModifier { + _openSwiftUIUnimplementedFailure() + } + + package mutating func removeFontModifier(ofType _: M.Type) where M: StaticFontModifier { + _openSwiftUIUnimplementedFailure() + } + } package struct ResolvedProperties { package var insets: EdgeInsets diff --git a/Sources/OpenSwiftUICore/View/Text/Util/Text+AlwaysOn.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+AlwaysOn.swift similarity index 100% rename from Sources/OpenSwiftUICore/View/Text/Util/Text+AlwaysOn.swift rename to Sources/OpenSwiftUICore/View/Text/Text/Text+AlwaysOn.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+CLKTextProvider.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+CLKTextProvider.swift new file mode 100644 index 000000000..362ba9d36 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+CLKTextProvider.swift @@ -0,0 +1,7 @@ +// +// Text+CLKTextProvider.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: 80BABD035AF3E1831C2DA2EAA39A253B (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift new file mode 100644 index 000000000..78d2ac56f --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift @@ -0,0 +1,227 @@ +// +// Text+Date.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: AEE0E21EC7C6B2D1204F94F94CBF7389 (SwiftUICore) + +public import Foundation +package import OpenAttributeGraphShims + +// MARK: - Text + DateStyle [WIP] + +extension Text { + + /// A predefined style used to display a `Date`. + public struct DateStyle: Sendable { + + /// A style displaying only the time component for a date. + /// + /// Text(event.startDate, style: .time) + /// + /// Example output: + /// 11:23PM + public static let time: Text.DateStyle = .init(storage: .time) + + /// A style displaying a date. + /// + /// Text(event.startDate, style: .date) + /// + /// Example output: + /// June 3, 2019 + public static let date: Text.DateStyle = .init(storage: .date) + + /// A style displaying a date as relative to now. + /// + /// Text(event.startDate, style: .relative) + /// + /// Example output: + /// 2 hours, 23 minutes + /// 1 year, 1 month + public static let relative: Text.DateStyle = .init(storage: .relative) + + /// A style displaying a date as offset from now. + /// + /// Text(event.startDate, style: .offset) + /// + /// Example output: + /// +2 hours + /// -3 months + public static let offset: Text.DateStyle = .init(storage: .offset) + + /// A style displaying a date as timer counting from now. + /// + /// Text(event.startDate, style: .timer) + /// + /// Example output: + /// 2:32 + /// 36:59:01 + public static let timer: Text.DateStyle = .init(storage: .timer) + + @_spi(Private) + public static func relative( + unitConfiguration: Text.DateStyle.UnitsConfiguration + ) -> Text.DateStyle { + Text.DateStyle( + storage: .relative, + unitConfiguration: unitConfiguration + ) + } + + @_spi(Private) + public static func timer( + units: NSCalendar.Unit + ) -> Text.DateStyle { + Text.DateStyle( + storage: .timer, + unitConfiguration: UnitsConfiguration(units: units, style: .full) + ) + } + + enum Storage { + case time + case date + case relative + case offset + case timer + } + + var storage: Storage + + @_spi(Private) + public struct UnitsConfiguration: Equatable, Codable, Sendable { + public enum Style: Int, Equatable, Codable, Sendable { + case short + case brief + case full + } + + @CodableRawRepresentable + package var units: NSCalendar.Unit + + package var style: Text.DateStyle.UnitsConfiguration.Style + + public init( + units: NSCalendar.Unit, + style: Text.DateStyle.UnitsConfiguration.Style + ) { + self._units = .init(units) + self.style = style + } + } + + package var unitConfiguration: UnitsConfiguration? + + @_spi(Private) + public var units: NSCalendar.Unit { + if let units = unitConfiguration?.units { + units + } else { + switch storage { + case .date: [.year, .month, .day] + case .timer: [.hour, .minute, .second] + default: [.year, .month, .day, .hour, .minute, .second] + } + } + + } + } + + public init(_ date: Date, style: Text.DateStyle) { + _openSwiftUIUnimplementedFailure() + } + + public init(_ dates: ClosedRange) { + _openSwiftUIUnimplementedFailure() + } + + public init(_ interval: DateInterval) { + _openSwiftUIUnimplementedFailure() + } + + @_spi(Private) + public init( + dateFormat: String, + timeZone: TimeZone? = nil + ) { + _openSwiftUIUnimplementedFailure() + } + + @_spi(Private) + public init( + dateFormatTemplate: String, + timeZone: TimeZone? = nil + ) { + _openSwiftUIUnimplementedFailure() + } +} + +// TDOO + +// MARK: - Text + ReferenceDate + +@available(OpenSwiftUI_v1_0, *) +extension View { + @_spi(OpenSwiftUIPrivate) + @available(OpenSwiftUI_v3_0, *) + nonisolated public func referenceDate(_ date: Date?) -> some View { + modifier(ReferenceDateModifier(date: date)) + } +} + +package struct ReferenceDateInput: ViewInput { + package static var defaultValue: WeakAttribute { + .init() + } +} + +extension _GraphInputs { + @inline(__always) + package var referenceDate: WeakAttribute { + get { self[ReferenceDateInput.self] } + set { self[ReferenceDateInput.self] = newValue } + } +} + +package struct ReferenceDateModifier: PrimitiveViewModifier, ViewInputsModifier { + package var date: Date? + + nonisolated package static func _makeViewInputs( + modifier: _GraphValue, + inputs: inout _ViewInputs + ) { + inputs.base.referenceDate = WeakAttribute( + modifier.value.unsafeBitCast(to: Date?.self) + ) + } +} + +// MARK: - Text.DateStyle + Extension + +@available(OpenSwiftUI_v2_0, *) +extension Text.DateStyle: Equatable { + public static func == (a: Text.DateStyle, b: Text.DateStyle) -> Bool { + a.storage == b.storage && a.unitConfiguration == b.unitConfiguration + } +} + +@available(OpenSwiftUI_v2_0, *) +extension Text.DateStyle: Codable { + enum Errors: Error { + case unknownStorage + } + + enum CodingKeys: CodingKey { + case storage + case unitConfiguration + } + + public func encode(to encoder: any Encoder) throws { + _openSwiftUIUnimplementedFailure() + } + + public init(from decoder: any Decoder) throws { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+DiscreteFormatStyle.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+DiscreteFormatStyle.swift new file mode 100644 index 000000000..655da1cb6 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+DiscreteFormatStyle.swift @@ -0,0 +1,7 @@ +// +// Text+DiscreteFormatStyle.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: C8A98712CE9284278805F6E671356D1B (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Formatter.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Formatter.swift new file mode 100644 index 000000000..79c3a1202 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Formatter.swift @@ -0,0 +1,7 @@ +// +// Text+Formatter.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: 7267202B6A40C9B73733978AB256B462 (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+LayoutShape.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+LayoutShape.swift new file mode 100644 index 000000000..954810e45 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+LayoutShape.swift @@ -0,0 +1,106 @@ +// +// Text+LayoutShape.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 0B075DB77A31A3DA949C6F4F810CBA16 (SwiftUICore) + +package import Foundation + +package struct TextShape: Equatable { + private enum Exclusion: Equatable { + case excludeTop(HorizontalEdge, CGSize) + } + + package static func excludeTop( + _ edge: HorizontalEdge, + size: CGSize + ) -> TextShape { + TextShape(exclusion: .excludeTop(edge, size)) + } + + package static var bounds: TextShape { + TextShape(exclusion: nil) + } + + private var exclusion: Exclusion? + + package struct Resolved: Equatable { + package enum Kind: Equatable { + case excludeTop(AbsoluteEdge, CGSize) + case bounds + } + + package var boundsSize: CGSize + + package var kind: TextShape.Resolved.Kind + + package init( + boundsSize: CGSize, + kind: TextShape.Resolved.Kind + ) { + self.boundsSize = boundsSize + self.kind = kind + } + + package init() { + self.boundsSize = .zero + self.kind = .bounds + } + + package func adjustLayout( + width: inout CGFloat, + height: inout CGFloat, + targetWidth: CGFloat? + ) { + guard case let .excludeTop(edge, size) = kind else { + return + } + switch edge { + case .left: + if let targetWidth { + width = targetWidth + } + height = max(height, size.height) + case .right: + if let targetWidth { + width = targetWidth + } else { + width = size.width + width + } + height = max(height, size.height) + default: _openSwiftUIUnreachableCode() + } + } + + package var exclusionPaths: [Path] { + guard case let .excludeTop(edge, size) = kind else { + return [] + } + let x = edge == .right ? boundsSize.width - size.width : .zero + let rect = CGRect(origin: CGPoint(x: x, y: .zero), size: size) + return [Path(rect)] + } + } + + package func resolve( + in size: CGSize, + layoutDirection: LayoutDirection + ) -> Resolved { + guard case let .excludeTop(edge, exclusionSize) = exclusion else { + return Resolved(boundsSize: .zero, kind: .bounds) + } + let absoluteEdge: AbsoluteEdge = switch edge { + case .leading: layoutDirection == .rightToLeft ? .right : .left + case .trailing: layoutDirection == .rightToLeft ? .left : .right + } + return Resolved( + boundsSize: CGSize( + width: size.width == .infinity ? .greatestFiniteMagnitude : size.width, + height: size.height == .infinity ? .greatestFiniteMagnitude : size.height + ), + kind: .excludeTop(absoluteEdge, exclusionSize) + ) + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+LineHeight.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+LineHeight.swift new file mode 100644 index 000000000..15fd1eb38 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+LineHeight.swift @@ -0,0 +1,7 @@ +// +// Text+LineHeight.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: 45A852A73BEF313599F8AEDEA4BAAE07 (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+PlatformRepresentation.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+PlatformRepresentation.swift index 974ef1e59..377b22bac 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+PlatformRepresentation.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+PlatformRepresentation.swift @@ -20,7 +20,7 @@ package protocol PlatformTextRepresentable { static func makeRepresentation( inputs: _ViewInputs, - context: Attribute, + context: Attribute, outputs: inout _ViewOutputs ) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift new file mode 100644 index 000000000..2ff8ca657 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift @@ -0,0 +1,7 @@ +// +// Text+Renderer.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: 7F70C8A76EE0356881289646072938C0 (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift new file mode 100644 index 000000000..59c70dff8 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift @@ -0,0 +1,153 @@ +// +// Text+SizeFitting.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Blocked by _TextVariantPreference implementation +// ID: 22A2F77020526CCA53FF38DE37184183 (SwiftUICore) + +// MARK: - TextVariantPreference + +@available(OpenSwiftUI_v6_0, *) +extension Text { + + /// Controls the way text size variants are chosen. + /// + /// Certain types of text, such as ``Text(_:format:)``, can generate strings of + /// different size to better fit the available space. By default, all text uses the + /// widest available variant. Setting the variant to be + /// ``TextVariantPreference/sizeDependent`` allows the text to take the available + /// space into account when choosing what content to display. + @available(OpenSwiftUI_v6_0, *) + public func textVariant( + _ preference: V + ) -> some View where V: TextVariantPreference { + preference._preference.body(self) + } +} + +/// A protocol for controlling the size variant of text views. +@available(OpenSwiftUI_v6_0, *) +public protocol TextVariantPreference { + var _preference: _TextVariantPreference { get } +} + +/// Internal requirement for ``TextVariantPreference``. +@available(OpenSwiftUI_v6_0, *) +public struct _TextVariantPreference: Sendable where Preference: TextVariantPreference { + fileprivate func body( + _ view: V + ) -> some View where V: View { + // TODO + _openSwiftUIUnimplementedFailure() + } +} + +/// The default text variant preference that chooses the largest available +/// variant. +@available(OpenSwiftUI_v6_0, *) +public struct FixedTextVariant: TextVariantPreference, Sendable { + public var _preference: _TextVariantPreference { + .init() + } +} + +/// The size dependent variant preference allows the text to take the available +/// space into account when choosing the variant to display. +@available(OpenSwiftUI_v6_0, *) +public struct SizeDependentTextVariant: TextVariantPreference, Sendable { + public var _preference: _TextVariantPreference { + .init() + } +} + +@available(OpenSwiftUI_v6_0, *) +extension TextVariantPreference where Self == FixedTextVariant { + + /// The default text variant preference. It always chooses the largest available + /// variant. + public static var fixed: FixedTextVariant { + .init() + } +} + +@available(OpenSwiftUI_v6_0, *) +extension TextVariantPreference where Self == SizeDependentTextVariant { + + /// The size dependent preference allows the text to take the available space into + /// account when choosing the size variant to display. + /// + /// When a ``Text`` provides different size options for its content, the size + /// dependent preference chooses the largest option that fits into the available + /// space without truncating or clipping its content. + /// + /// - Note: Only use this option where needed as it incurs a performance cost on + /// every ``Text`` it is applied to, even if the concrete text initializer cannot + /// provide multiple size variants and there is no visual impact. + /// + /// ## Difference to ViewThatFits + /// + /// The ``sizeDependent`` text variant preference differs from ``ViewThatFits`` both + /// in usage and in behavior. ``ViewThatFits`` chooses the first child where the + /// **ideal** size fits the available space. For ``Text`` this means that it will + /// only choose texts that can fit their contents into the available space **without + /// a line break**. With this text variant preference, on the other hand, the + /// largest variant is chosen that can fit the available space while respecting all + /// the regular layout rules, such as ``EnvironmentValues/lineLimit``. + /// + /// To use ``ViewThatFits``, multiple different views have to be provided as the + /// different size options. With this text variant preference, a single ``Text`` + /// provides the different size variants intrinsically. The way it generates these + /// size variants and how many size variants are available depends on the text + /// initializer used. + public static var sizeDependent: SizeDependentTextVariant { + .init() + } +} + +// MARK: - TextSizeVariant + +package struct TextSizeVariant: Comparable, Hashable, RawRepresentable { + package var rawValue: Int + + package init(rawValue: Int) { + self.rawValue = rawValue + } + + package static let regular: TextSizeVariant = .init(rawValue: 0) + + package static let compact: TextSizeVariant = .init(rawValue: 1) + + package static let small: TextSizeVariant = .init(rawValue: 2) + + package static let tiny: TextSizeVariant = .init(rawValue: 3) + + package static func < (lhs: TextSizeVariant, rhs: TextSizeVariant) -> Bool { + lhs.rawValue < rhs.rawValue + } + + package var nextUp: TextSizeVariant? { + if rawValue == 0 { + return nil + } else { + return TextSizeVariant(rawValue: rawValue - 1) + } + } + + package var nextDown: TextSizeVariant { + TextSizeVariant(rawValue: rawValue + 1) + } +} + +extension TextSizeVariant: Codable { + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(Int.self) + self.init(rawValue: rawValue) + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift index 65773ca8d..91e894a11 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 22747AAF70EE5063D02F299CE90A18BE (SwiftUICore) // MARK: - Text + Sizing @@ -23,7 +23,7 @@ extension Text { package var storage: Text.Sizing.Storage package init(_ storage: Text.Sizing.Storage) { - _openSwiftUIUnimplementedFailure() + self.storage = storage } public static let standard: Text.Sizing = .init(.standard) @@ -44,3 +44,18 @@ extension EnvironmentValues { set { self[TextSizingKey.self] = newValue } } } + +// MARK: - PreferTextLayoutManagerInput + +private struct PreferTextLayoutManagerInputModifier: ViewInputsModifier { + static func _makeViewInputs( + modifier: _GraphValue, + inputs: inout _ViewInputs + ) { + inputs[PreferTextLayoutManagerInput.self] = true + } +} + +package struct PreferTextLayoutManagerInput: ViewInput { + package static var defaultValue: Bool { false } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index ea518bfe7..c7a02ba2f 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -6,6 +6,9 @@ // Status: WIP // ID: 641995D812913A47B866B20B88782376 (SwiftUICore) +import OpenAttributeGraphShims +public import OpenCoreGraphicsShims + // MARK: - Text + View [WIP] @available(OpenSwiftUI_v1_0, *) @@ -14,11 +17,140 @@ extension Text: UnaryView, PrimitiveView { view: _GraphValue, inputs: _ViewInputs ) -> _ViewOutputs { - // WIP - return .init() + if let representation = inputs.requestedTextRepresentation, + representation.shouldMakeRepresentation(inputs: inputs) { + var outputs = makeCommonAttributes(view: view, inputs: inputs) + let options = representation.representationOptions(inputs: inputs) + _openSwiftUIUnimplementedWarning() + return outputs + } else { + return makeCommonAttributes(view: view, inputs: inputs) + } + } + + private static func makeCommonAttributes( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + .init() + } +} + +// MARK: - TextLayoutProperties + +@_spi(Private) +@available(OpenSwiftUI_v3_0, *) +public struct TextLayoutProperties: Equatable { + public var lineLimit: Int? + + package var lowerLineLimit: Int? + +// public var truncationMode: Text.TruncationMode = + + public var multilineTextAlignment: TextAlignment + + public var layoutDirection: LayoutDirection + + package var transitionStyle: ContentTransition.Style + + public var minScaleFactor: CGFloat = 1.0 + + public var lineSpacing: CGFloat = .zero + + public var lineHeightMultiple: CGFloat = .zero + +// public var maximumLineHeight: CGFloat = + +// public var minimumLineHeight: CGFloat + + public var hyphenationFactor: CGFloat = .zero + + package var hyphenationDisabled: Bool = false + + package var writingMode: Text.WritingMode = .horizontalTopToBottom + + package var bodyHeadOutdent: CGFloat = .zero + + package var pixelLength: CGFloat = 1.0 + + package var textSizing: Text.Sizing = .standard + + // package var textShape: TextShape + + // package var flags: Flags + + package var widthIsFlexible: Bool { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package var sizeFitting: Bool { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package init() { + _openSwiftUIUnimplementedFailure() + + } + + private struct Key: DerivedEnvironmentKey { + static func value(in environment: EnvironmentValues) -> TextLayoutProperties { + // TODO + .init() + } + } + + public init(_ env: EnvironmentValues) { + self = env[Key.self] + } + + package func update( + _ env: inout EnvironmentValues, + from old: TextLayoutProperties + ) { + _openSwiftUIUnimplementedFailure() + } + + public static func == (a: TextLayoutProperties, b: TextLayoutProperties) -> Bool { + _openSwiftUIUnimplementedFailure() + } +} + +@_spi(Private) +@available(*, unavailable) +extension TextLayoutProperties: Sendable {} + +@_spi(Private) +extension TextLayoutProperties: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + _openSwiftUIUnimplementedFailure() + } + + package init(from decoder: inout ProtobufDecoder) throws { + _openSwiftUIUnimplementedFailure() } } + +// MARK: - ResolvedTextFilter [WIP] + +struct ResolvedTextFilter: StatefulRule, AsyncAttribute { + @Attribute var text: Text + @Attribute var environment: EnvironmentValues + var helper: ResolvedTextHelper + + typealias Value = ResolvedStyledText + + func updateValue() { + ResolvedStyledText() + } +} + +struct ResolvedTextHelper { + +} + package class TextRendererBoxBase {} package struct AccessibilityStyledTextContentView: View where Provider: TextAccessibilityProvider { @@ -45,7 +177,15 @@ package struct AccessibilityStyledTextContentView: View where Provider } // FIXME: -package class ResolvedStyledText {} + +@available(OpenSwiftUI_v6_0, *) +@usableFromInline +package class ResolvedStyledText: CustomStringConvertible { + @usableFromInline + package var description: String { + _openSwiftUIUnimplementedFailure() + } +} extension ResolvedStyledText { package class StringDrawing {} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextAnimationsProvider.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextAnimationsProvider.swift new file mode 100644 index 000000000..57bc9c357 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextAnimationsProvider.swift @@ -0,0 +1,7 @@ +// +// TextAnimationsProvider.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: 1A78AA63D8D20D041CE9C3A93C876C83 (SwiftUICore) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextViewModifier.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextViewModifier.swift deleted file mode 100644 index 33a48a217..000000000 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextViewModifier.swift +++ /dev/null @@ -1,8 +0,0 @@ -extension String { - package func caseConvertedIfNeeded( - _ environment: EnvironmentValues - ) -> String { - _openSwiftUIUnimplementedWarning() - return self - } -} diff --git a/Sources/OpenSwiftUICore/View/Text/Util/TextFormatter.swift b/Sources/OpenSwiftUICore/View/Text/Util/TextFormatter.swift deleted file mode 100644 index c5c1d64a4..000000000 --- a/Sources/OpenSwiftUICore/View/Text/Util/TextFormatter.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// TextFormatter.swift -// OpenSwiftUICore -// -// Audited for 6.5.4 -// Status: WIP - -public import Foundation -package import OpenAttributeGraphShims - -// MARK: - ReferenceDate - -@available(OpenSwiftUI_v1_0, *) -extension View { - @_spi(OpenSwiftUIPrivate) - @available(OpenSwiftUI_v3_0, *) - nonisolated public func referenceDate(_ date: Date?) -> some View { - modifier(ReferenceDateModifier(date: date)) - } -} - -package struct ReferenceDateInput: ViewInput { - package static var defaultValue: WeakAttribute { - .init() - } -} - -extension _GraphInputs { - @inline(__always) - package var referenceDate: WeakAttribute { - get { self[ReferenceDateInput.self] } - set { self[ReferenceDateInput.self] = newValue } - } -} - -package struct ReferenceDateModifier: PrimitiveViewModifier, ViewInputsModifier { - package var date: Date? - - nonisolated package static func _makeViewInputs( - modifier: _GraphValue, - inputs: inout _ViewInputs - ) { - inputs.base.referenceDate = WeakAttribute( - modifier.value.unsafeBitCast(to: Date?.self) - ) - } -} diff --git a/Sources/OpenSwiftUICore/View/Text/Util/TruncationMode.swift b/Sources/OpenSwiftUICore/View/Text/Util/TruncationMode.swift new file mode 100644 index 000000000..a81205f43 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Util/TruncationMode.swift @@ -0,0 +1,637 @@ +// +// TruncationMode.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 52803FDE2123C3846E0286DE7934BA01 (SwiftUICore?) + +public import Foundation + +@available(OpenSwiftUI_v1_0, *) +extension Text { + + // MARK: - Text.TruncationMode + + /// The type of truncation to apply to a line of text when it's too long to + /// fit in the available space. + /// + /// When a text view contains more text than it's able to display, the view + /// might truncate the text and place an ellipsis (...) at the truncation + /// point. Use the ``View/truncationMode(_:)`` modifier with one of the + /// `TruncationMode` values to indicate which part of the text to + /// truncate, either at the beginning, in the middle, or at the end. + public enum TruncationMode: Sendable { + + /// Truncate at the beginning of the line. + /// + /// Use this kind of truncation to omit characters from the beginning of + /// the string. For example, you could truncate the English alphabet as + /// "...wxyz". + case head + + /// Truncate at the end of the line. + /// + /// Use this kind of truncation to omit characters from the end of the + /// string. For example, you could truncate the English alphabet as + /// "abcd...". + case tail + + /// Truncate in the middle of the line. + /// + /// Use this kind of truncation to omit characters from the middle of + /// the string. For example, you could truncate the English alphabet as + /// "ab...yz". + case middle + } + + // MARK: - Text.Case + + /// A scheme for transforming the capitalization of characters within text. + @available(OpenSwiftUI_v2_0, *) + public enum Case: Sendable { + + /// Displays text in all uppercase characters. + /// + /// For example, "Hello" would be displayed as "HELLO". + /// + /// - SeeAlso: `StringProtocol.uppercased(with:)` + case uppercase + + /// Displays text in all lowercase characters. + /// + /// For example, "Hello" would be displayed as "hello". + /// + /// - SeeAlso: `StringProtocol.lowercased(with:)` + case lowercase + } +} + +// MARK: - Text.TruncationMode + ProtobufEnum + +extension Text.TruncationMode: ProtobufEnum { + package var protobufValue: UInt { + switch self { + case .head: 1 + case .tail: 2 + case .middle: 3 + } + } + + package init?(protobufValue value: UInt) { + switch value { + case 1: self = .head + case 2: self = .tail + case 3: self = .middle + default: return nil + } + } +} + +// MARK: - CodableTextCase + +package enum CodableTextCase: Codable { + case uppercase + case lowercase + + package init(_ textCase: Text.Case) { + self = switch textCase { + case .uppercase: .uppercase + case .lowercase: .lowercase + } + } + + package var textCase: Text.Case { + switch self { + case .uppercase: .uppercase + case .lowercase: .lowercase + } + } + + private enum CodingKeys: CodingKey { + case uppercase + case lowercase + } + + private enum UppercaseCodingKeys: CodingKey {} + + private enum LowercaseCodingKeys: CodingKey {} + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .uppercase: + _ = container.nestedContainer(keyedBy: UppercaseCodingKeys.self, forKey: .uppercase) + case .lowercase: + _ = container.nestedContainer(keyedBy: LowercaseCodingKeys.self, forKey: .lowercase) + } + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let keys = container.allKeys + guard keys.count == 1 else { + throw DecodingError.typeMismatch( + Self.self, + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one." + ) + ) + } + switch keys[0] { + case .uppercase: + _ = try container.nestedContainer(keyedBy: UppercaseCodingKeys.self, forKey: .uppercase) + self = .uppercase + case .lowercase: + _ = try container.nestedContainer(keyedBy: LowercaseCodingKeys.self, forKey: .lowercase) + self = .lowercase + } + } +} + +extension Text.Case: CodableByProxy { + package var codingProxy: CodableTextCase { .init(self) } + + package static func unwrap(codingProxy: CodableTextCase) -> Text.Case { + codingProxy.textCase + } +} + +// MARK: - EnvironmentValues + Text Properties + +@available(OpenSwiftUI_v1_0, *) +extension EnvironmentValues { + + /// An environment value that indicates how a text view aligns its lines + /// when the content wraps or contains newlines. + /// + /// Set this value for a view hierarchy by applying the + /// ``View/multilineTextAlignment(_:)`` view modifier. Views in the + /// hierarchy that display text, like ``Text`` or ``TextEditor``, read the + /// value from the environment and adjust their text alignment accordingly. + /// + /// This value has no effect on a ``Text`` view that contains only one + /// line of text, because a text view has a width that exactly matches the + /// width of its widest line. If you want to align an entire text view + /// rather than its contents, set the aligment of its container, like a + /// ``VStack`` or a frame that you create with the + /// ``View/frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)`` + /// modifier. + /// + /// > Note: You can use this value to control the alignment of a ``Text`` + /// view that you create with the ``Text/init(_:style:)`` initializer + /// to display localized dates and times, including when the view uses + /// only a single line, but only when that view appears in a widget. + public var multilineTextAlignment: TextAlignment { + get { self[TextAlignmentKey.self] ?? .leading } + set { self[TextAlignmentKey.self] = newValue } + } + + /// A value that indicates how the layout truncates the last line of text to + /// fit into the available space. + /// + /// The default value is ``Text/TruncationMode/tail``. Some controls, + /// however, might have a different default if appropriate. + public var truncationMode: Text.TruncationMode { + get { self[TruncationModeKey.self] ?? .tail } + set { self[TruncationModeKey.self] = newValue } + } + + package var explicitTruncationMode: Text.TruncationMode? { + get { self[TruncationModeKey.self] } + set { self[TruncationModeKey.self] = newValue } + } + + package var defaultTextFieldTruncationMode: Text.TruncationMode? { + get { self[DefaultTextFieldTruncationMode.self] } + set { self[DefaultTextFieldTruncationMode.self] = newValue } + } + + /// The distance in points between the bottom of one line fragment and the + /// top of the next. + /// + /// This value is always nonnegative. + public var lineSpacing: CGFloat { + get { self[LineSpacingKey.self] } + set { self[LineSpacingKey.self] = newValue } + } + + /// The natural line height of text is multiplied by this factor + /// (if positive). + /// + /// The default value is `0.0`. + @available(OpenSwiftUI_v2_0, *) + public var _lineHeightMultiple: CGFloat { + get { self[LineHeightMultipleKey.self] } + set { self[LineHeightMultipleKey.self] = newValue } + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + public var lineHeightMultiple: CGFloat { + get { self[LineHeightMultipleKey.self] } + set { self[LineHeightMultipleKey.self] = newValue } + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + public var maximumLineHeight: CGFloat { + get { self[MaximumLineHeightKey.self] } + set { self[MaximumLineHeightKey.self] = newValue } + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + public var minimumLineHeight: CGFloat { + get { self[MinimumLineHeightKey.self] } + set { self[MinimumLineHeightKey.self] = newValue } + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + public var hyphenationFactor: CGFloat { + get { self[HyphenationFactorKey.self] } + set { self[HyphenationFactorKey.self] = newValue } + } + + /// A Boolean value that indicates whether inter-character spacing should + /// tighten to fit the text into the available space. + /// + /// The default value is `false`. + public var allowsTightening: Bool { + get { self[AllowsTighteningKey.self] } + set { self[AllowsTighteningKey.self] = newValue } + } + + @_spi(Private) + @available(OpenSwiftUI_v4_0, *) + public var avoidsOrphans: Bool { + get { self[AvoidsOrphansKey.self] } + set { self[AvoidsOrphansKey.self] = newValue } + } + + var bodyHeadOutdent: CGFloat { + get { self[BodyHeadOutdentKey.self] } + set { self[BodyHeadOutdentKey.self] = newValue } + } + + /// The minimum permissible proportion to shrink the font size to fit + /// the text into the available space. + /// + /// In the example below, a label with a `minimumScaleFactor` of `0.5` + /// draws its text in a font size as small as half of the actual font if + /// needed to fit into the space next to the text input field: + /// + /// HStack { + /// Text("This is a very long label:") + /// .lineLimit(1) + /// .minimumScaleFactor(0.5) + /// TextField("My Long Text Field", text: $myTextField) + /// .frame(width: 250, height: 50, alignment: .center) + /// } + /// + /// ![A screenshot showing the effects of setting the minimumScaleFactor on + /// the text in a view](OpenSwiftUI-View-minimumScaleFactor.png) + /// + /// You can set the minimum scale factor to any value greater than `0` and + /// less than or equal to `1`. The default value is `1`. + /// + /// OpenSwiftUI uses this value to shrink text that doesn't fit in a view when + /// it's okay to shrink the text. For example, a label with a + /// `minimumScaleFactor` of `0.5` draws its text in a font size as small as + /// half the actual font if needed. + public var minimumScaleFactor: CGFloat { + get { self[MinimumScaleFactorKey.self] } + set { self[MinimumScaleFactorKey.self] = newValue } + } + + /// A stylistic override to transform the case of `Text` when displayed, + /// using the environment's locale. + /// + /// The default value is `nil`, displaying the `Text` without any case + /// changes. + @available(OpenSwiftUI_v2_0, *) + public var textCase: Text.Case? { + get { self[TextCaseKey.self] } + set { self[TextCaseKey.self] = newValue } + } +} + +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Sets the alignment of a text view that contains multiple lines of text. + /// + /// Use this modifier to set an alignment for a multiline block of text. + /// For example, the modifier centers the contents of the following + /// ``Text`` view: + /// + /// Text("This is a block of text that shows up in a text element as multiple lines.\("\n") Here we have chosen to center this text.") + /// .frame(width: 200) + /// .multilineTextAlignment(.center) + /// + /// The text in the above example spans more than one line because: + /// + /// * The newline character introduces a line break. + /// * The frame modifier limits the space available to the text view, and + /// by default a text view wraps lines that don't fit in the available + /// width. As a result, the text before the explicit line break wraps to + /// three lines, and the text after uses two lines. + /// + /// The modifier applies the alignment to the all the lines of text in + /// the view, regardless of why wrapping occurs: + /// + /// ![A block of text that spans 5 lines. The lines of text are center-aligned.](View-multilineTextAlignment-1-iOS) + /// + /// The modifier has no effect on a ``Text`` view that contains only one + /// line of text, because a text view has a width that exactly matches the + /// width of its widest line. If you want to align an entire text view + /// rather than its contents, set the aligment of its container, like a + /// ``VStack`` or a frame that you create with the + /// ``View/frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)`` + /// modifier. + /// + /// > Note: You can use this modifier to control the alignment of a ``Text`` + /// view that you create with the ``Text/init(_:style:)`` initializer + /// to display localized dates and times, including when the view uses + /// only a single line, but only when that view appears in a widget. + /// + /// The modifier also affects the content alignment of other text container + /// types, like ``TextEditor`` and ``TextField``. In those cases, the + /// modifier sets the alignment even when the view contains only a single + /// line because view's width isn't dictated by the width of the text it + /// contains. + /// + /// The modifier operates by setting the + /// ``EnvironmentValues/multilineTextAlignment`` value in the environment, + /// so it affects all the text containers in the modified view hierarchy. + /// For example, you can apply the modifier to a ``VStack`` to + /// configure all the text views inside the stack. + /// + /// - Parameter alignment: A value that you use to align multiple lines of + /// text within a view. + /// + /// - Returns: A view that aligns the lines of multiline ``Text`` instances + /// it contains. + @inlinable + nonisolated public func multilineTextAlignment(_ alignment: TextAlignment) -> some View { + environment(\.multilineTextAlignment, alignment) + } + + /// Sets the truncation mode for lines of text that are too long to fit in + /// the available space. + /// + /// Use the `truncationMode(_:)` modifier to determine whether text in a + /// long line is truncated at the beginning, middle, or end. Truncation is + /// indicated by adding an ellipsis (…) to the line when removing text to + /// indicate to readers that text is missing. + /// + /// In the example below, the bounds of text view constrains the amount of + /// text that the view displays and the `truncationMode(_:)` specifies from + /// which direction and where to display the truncation indicator: + /// + /// Text("This is a block of text that will show up in a text element as multiple lines. The text will fill the available space, and then, eventually, be truncated.") + /// .frame(width: 150, height: 150) + /// .truncationMode(.tail) + /// + /// ![A screenshot showing the effect of truncation mode on text in a + /// view.](OpenSwiftUI-view-truncationMode.png) + /// + /// - Parameter mode: The truncation mode that specifies where to truncate + /// the text within the text view, if needed. You can truncate at the + /// beginning, middle, or end of the text view. + /// + /// - Returns: A view that truncates text at different points in a line + /// depending on the mode you select. + @inlinable + nonisolated public func truncationMode(_ mode: Text.TruncationMode) -> some View { + environment(\.truncationMode, mode) + } + + /// Sets the amount of space between lines of text in this view. + /// + /// Use `lineSpacing(_:)` to set the amount of spacing from the bottom of + /// one line to the top of the next for text elements in the view. + /// + /// In the ``Text`` view in the example below, 10 points separate the bottom + /// of one line to the top of the next as the text field wraps inside this + /// view. Applying `lineSpacing(_:)` to a view hierarchy applies the line + /// spacing to all text elements contained in the view. + /// + /// Text("This is a string in a TextField with 10 point spacing applied between the bottom of one line and the top of the next.") + /// .frame(width: 200, height: 200, alignment: .leading) + /// .lineSpacing(10) + /// + /// ![A screenshot showing the effects of setting line spacing on the text + /// in a view.](OpenSwiftUI-view-lineSpacing.png) + /// + /// - Parameter lineSpacing: The amount of space between the bottom of one + /// line and the top of the next line in points. + @inlinable + nonisolated public func lineSpacing(_ lineSpacing: CGFloat) -> some View { + environment(\.lineSpacing, lineSpacing) + } + + /// Sets the factor to multiply the natural line height of each line by. + /// + /// - Parameter multiple: The natural line height of the receiver is + /// multiplied by this factor (if positive). The default value is `0.0`. + @available(OpenSwiftUI_v2_0, *) + @usableFromInline + @available(*, deprecated, renamed: "lineHeightMultiple") + @MainActor + @preconcurrency internal func _lineHeightMultiple(_ multiple: CGFloat) -> some View { + environment(\._lineHeightMultiple, multiple) + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + nonisolated public func lineHeightMultiple(_ multiple: CGFloat) -> some View { + environment(\.lineHeightMultiple, multiple) + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + nonisolated public func maximumLineHeight(_ lineHeight: CGFloat) -> some View { + environment(\.maximumLineHeight, lineHeight) + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + nonisolated public func minimumLineHeight(_ lineHeight: CGFloat) -> some View { + environment(\.minimumLineHeight, lineHeight) + } + + @_spi(Private) + @available(OpenSwiftUI_v2_0, *) + nonisolated public func hyphenationFactor(_ factor: CGFloat) -> some View { + environment(\.hyphenationFactor, factor) + } + + /// Sets whether text in this view can compress the space between characters + /// when necessary to fit text in a line. + /// + /// Use `allowsTightening(_:)` to enable the compression of inter-character + /// spacing of text in a view to try to fit the text in the view's bounds. + /// + /// In the example below, two identically configured text views show the + /// effects of `allowsTightening(_:)` on the compression of the spacing + /// between characters: + /// + /// VStack { + /// Text("This is a wide text element") + /// .font(.body) + /// .frame(width: 200, height: 50, alignment: .leading) + /// .lineLimit(1) + /// .allowsTightening(true) + /// + /// Text("This is a wide text element") + /// .font(.body) + /// .frame(width: 200, height: 50, alignment: .leading) + /// .lineLimit(1) + /// .allowsTightening(false) + /// } + /// + /// ![A screenshot showing the effect of enabling text tightening in a + /// view.](OpenSwiftUI-view-allowsTightening.png) + /// + /// - Parameter flag: A Boolean value that indicates whether the space + /// between characters compresses when necessary. + /// + /// - Returns: A view that can compress the space between characters when + /// necessary to fit text in a line. + @inlinable + nonisolated public func allowsTightening(_ flag: Bool) -> some View { + environment(\.allowsTightening, flag) + } + + @_spi(Private) + @available(OpenSwiftUI_v4_0, *) + nonisolated public func avoidsOrphans(_ flag: Bool) -> some View { + environment(\.avoidsOrphans, flag) + } + + /// Sets the minimum amount that text in this view scales down to fit in the + /// available space. + /// + /// Use the `minimumScaleFactor(_:)` modifier if the text you place in a + /// view doesn't fit and it's okay if the text shrinks to accommodate. For + /// example, a label with a minimum scale factor of `0.5` draws its text in + /// a font size as small as half of the actual font if needed. + /// + /// In the example below, the ``HStack`` contains a ``Text`` label with a + /// line limit of `1`, that is next to a ``TextField``. To allow the label + /// to fit into the available space, the `minimumScaleFactor(_:)` modifier + /// shrinks the text as needed to fit into the available space. + /// + /// HStack { + /// Text("This is a long label that will be scaled to fit:") + /// .lineLimit(1) + /// .minimumScaleFactor(0.5) + /// TextField("My Long Text Field", text: $myTextField) + /// } + /// + /// ![A screenshot showing the effect of setting a minimumScaleFactor on + /// text in a view.](OpenSwiftUI-View-minimumScaleFactor.png) + /// + /// - Parameter factor: A fraction between 0 and 1 (inclusive) you use to + /// specify the minimum amount of text scaling that this view permits. + /// + /// - Returns: A view that limits the amount of text downscaling. + @inlinable + nonisolated public func minimumScaleFactor(_ factor: CGFloat) -> some View { + environment(\.minimumScaleFactor, factor) + } + + @_spi(Private) + nonisolated public func bodyHeadOutdent(_ amount: CGFloat) -> some View { + environment(\.bodyHeadOutdent, amount) + } + + /// Sets a transform for the case of the text contained in this view when + /// displayed. + /// + /// The default value is `nil`, displaying the text without any case + /// changes. + /// + /// - Parameter textCase: One of the ``Text/Case`` enumerations; the + /// default is `nil`. + /// - Returns: A view that transforms the case of the text. + @available(OpenSwiftUI_v2_0, *) + @inlinable + nonisolated public func textCase(_ textCase: Text.Case?) -> some View { + environment(\.textCase, textCase) + } +} + +package struct MaximumLineHeightKey: EnvironmentKey { + package static let defaultValue: CGFloat = .zero +} + +package struct MinimumLineHeightKey: EnvironmentKey { + package static let defaultValue: CGFloat = .zero +} + +package struct MinimumScaleFactorKey: EnvironmentKey { + package static let defaultValue: CGFloat = 1.0 +} + +private struct DefaultTextFieldTruncationMode: EnvironmentKey { + package static var defaultValue: Text.TruncationMode? { nil } +} + +private struct TextCaseKey: EnvironmentKey { + static var defaultValue: Text.Case? { nil } +} + +private struct TruncationModeKey: EnvironmentKey { + static var defaultValue: Text.TruncationMode? { nil } +} + +private struct HyphenationDisabledKey: EnvironmentKey { + static var defaultValue: Bool { false } +} + +private struct HyphenationFactorKey: EnvironmentKey { + static var defaultValue: CGFloat { .zero } +} + +private struct LineHeightMultipleKey: EnvironmentKey { + static var defaultValue: CGFloat { .zero } +} + +private struct LineSpacingKey: EnvironmentKey { + static var defaultValue: CGFloat { .zero } +} + +private struct TextAlignmentKey: EnvironmentKey { + static var defaultValue: TextAlignment? { nil } +} + +private struct AllowsTighteningKey: EnvironmentKey { + static var defaultValue: Bool { false } +} + +private struct AvoidsOrphansKey: EnvironmentKey { + static var defaultValue: Bool { true } +} + +private struct BodyHeadOutdentKey: EnvironmentKey { + static var defaultValue: CGFloat { .zero } +} + +// MARK: - String + Util + +extension String { + package func caseConvertedIfNeeded(_ environment: EnvironmentValues) -> String { + guard let textCase = environment.textCase else { + return self + } + let result = switch textCase { + case .uppercase: uppercased(with: environment.locale) + case .lowercase: lowercased(with: environment.locale) + } + return result + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Util/WritingMode.swift b/Sources/OpenSwiftUICore/View/Text/Util/WritingMode.swift new file mode 100644 index 000000000..e7e8d1e74 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Util/WritingMode.swift @@ -0,0 +1,89 @@ +// +// WritingMode.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 82074A2E22E8635055FCB3A2D5E40280 (SwiftUICore) + +@available(OpenSwiftUI_v1_0, *) +extension Text { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public struct WritingMode: Sendable, Hashable { + @_spi(Private) + package enum Storage { + case horizontalTopToBottom + case verticalRightToLeft + } + + package var storage: Text.WritingMode.Storage + + public static let horizontalTopToBottom: Text.WritingMode = .init(storage: .horizontalTopToBottom) + + public static let verticalRightToLeft: Text.WritingMode = .init(storage: .verticalRightToLeft) + } +} + +private struct WritingModeKey: EnvironmentKey { + static let defaultValue: Text.WritingMode = .horizontalTopToBottom +} + +extension EnvironmentValues { + package var writingMode: Text.WritingMode { + get { self[WritingModeKey.self] } + set { self[WritingModeKey.self] = newValue } + } +} + +@available(OpenSwiftUI_v5_0, *) +extension View { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + nonisolated public func writingMode(_ mode: Text.WritingMode) -> some View { + environment(\.writingMode, mode) + } +} + +@_spi(Private) +extension Text.WritingMode: ProtobufEnum { + package var protobufValue: UInt { + switch storage { + case .horizontalTopToBottom: 0 + case .verticalRightToLeft: 1 + } + } + + package init?(protobufValue value: UInt) { + switch value { + case 0: self = .horizontalTopToBottom + case 1: self = .verticalRightToLeft + default: return nil + } + } +} + +//#if canImport(UIFoundation_Private) +//import UIFoundation_Private +// +//extension NSTextHorizontalAlignment { +// package init( +// _ alignment: TextAlignment, +// layoutDirection: LayoutDirection, +// writingMode: Text.WritingMode +// ) { +// _openSwiftUIUnimplementedFailure() +// } +// +// package init(in environment: EnvironmentValues) { +// _openSwiftUIUnimplementedFailure() +// } +//} +// +//#endif + +//extension NSWritingDirection { +// package init(_ layoutDirection: LayoutDirection) { +// +// } +//} diff --git a/Sources/OpenSwiftUICore/Widget/HasWidgetMetadata.swift b/Sources/OpenSwiftUICore/Widget/HasWidgetMetadata.swift new file mode 100644 index 000000000..b6a54e7bf --- /dev/null +++ b/Sources/OpenSwiftUICore/Widget/HasWidgetMetadata.swift @@ -0,0 +1,25 @@ +// +// HasWidgetMetadata.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: FD72118870A434CF0E2B5B97BD09B3FE (SwiftUICore?) + +extension _ViewInputs { + package var hasWidgetMetadata: Bool { + get { base.hasWidgetMetadata } + set { base.hasWidgetMetadata = newValue } + } +} + +extension _GraphInputs { + private struct HasWidgetMetadataKey: GraphInput { + static var defaultValue: Bool { false } + } + + package var hasWidgetMetadata: Bool { + get { self[HasWidgetMetadataKey.self] } + set { self[HasWidgetMetadataKey.self] = newValue } + } +}