From 23fe9c092cfe2316f082307f708e0d6059ca7704 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 17 May 2026 13:01:18 +0800 Subject: [PATCH 01/24] Update Text.Modifier --- .../OpenSwiftUICore/View/Text/Text/Text.swift | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index a865ceb6c..ec68a5a6e 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -91,7 +91,7 @@ public struct Text: Equatable, Sendable { Log.runtimeIssues("Only unstyled text can be used with %s", [context]) } - // MARK: - Text.Modifier [WIP] + // MARK: - Text.Modifier @usableFromInline @frozen @@ -107,8 +107,50 @@ public struct Text: Equatable, Sendable { case anyTextModifier(AnyTextModifier) func modify(style: inout Text.Style, environment: EnvironmentValues) { - // Blocked by Text.Style - _openSwiftUIUnimplementedWarning() + switch self { + case let .color(color): + guard let color else { + style.color = Semantics.TextModifiersOverrideParentValues.isEnabled ? .default : .implicit + return + } + let baseStyle = style.color.baseStyle(in: environment) + let copiedStyle = AnyShapeStyle(color).copyStyle( + name: .foreground, + in: environment, + foregroundStyle: baseStyle + ) + style.color = .explicit(copiedStyle) + case let .font(font): + guard let font else { + style.baseFont = Semantics.FontModifiersNilResetValues.isEnabled ? .default : .implicit + return + } + style.baseFont = .explicit(font) + case .italic: + style.addFontModifier(type: Font.ItalicModifier.self) + case let .weight(weight): + if let weight { + style.addFontModifier(Font.WeightModifier(weight: weight)) + } else { + style.removeFontModifier(ofType: Font.WeightModifier.self) + style.removeFontModifier(ofType: Font.BoldModifier.self) + } + case let .kerning(value): + if Semantics.TextModifiersOverrideParentValues.isEnabled { + style.kerning = value + } else { + style.kerning = (style.kerning ?? .zero) + value + } + case let .tracking(value): + style.tracking = value + case let .baseline(value): + _ = Semantics.TextModifiersOverrideParentValues.isEnabled + style.baselineOffset = value + case .rounded: + style.addFontModifier(Font.DesignModifier(design: .rounded)) + case let .anyTextModifier(anyTextModifier): + anyTextModifier.modify(style: &style, environment: environment) + } } @usableFromInline From 412789871ac4522475269d1daafd7fa78d95a0c6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 21:06:35 +0800 Subject: [PATCH 02/24] Update ResolvedOption --- Sources/OpenSwiftUICore/View/Text/Text/Text.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index ec68a5a6e..e9e184f94 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -17,6 +17,7 @@ public typealias NSInteger = Int @available(OpenSwiftUI_v1_0, *) @frozen public struct Text: Equatable, Sendable { + // MARK: - Text.Storage @usableFromInline @@ -203,6 +204,9 @@ public struct Text: Equatable, Sendable { package static let allowsTextSuffix: Text.ResolveOptions = .init(rawValue: 1 << 6) package static let includeSupportForRepeatedResolution: Text.ResolveOptions = .init(rawValue: 1 << 7) + + @available(OpenSwiftUI_v7_0, *) + package static let ignoreMarkdown: Text.ResolveOptions = .init(rawValue: 1 << 8) } @usableFromInline From 2544dc776c85bb06bdc1983eca013155a6c7ecd8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 22:03:51 +0800 Subject: [PATCH 03/24] Update Text.resolve --- .../OpenSwiftUICore/View/Text/Text/Text.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index e9e184f94..204a7e6a9 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -12,7 +12,7 @@ public import Foundation public typealias NSInteger = Int #endif -// MARK: - Text [WIP] +// MARK: - Text @available(OpenSwiftUI_v1_0, *) @frozen @@ -236,7 +236,15 @@ public struct Text: Equatable, Sendable { with options: Text.ResolveOptions = [], idiom: AnyInterfaceIdiom? = nil ) -> (string: String, hasResolvableAttributes: Bool) { - _openSwiftUIUnimplementedFailure() + switch storage { + case .verbatim(let string): + return (string, false) + case .anyTextStorage: + var resolved = Text.ResolvedString() + resolved.idiom = idiom + resolve(into: &resolved, in: environment, with: options) + return (resolved.string, resolved.hasResolvableAttributes) + } } package func resolveString( @@ -247,10 +255,10 @@ public struct Text: Equatable, Sendable { switch storage { case let .verbatim(string): return string - case let .anyTextStorage(anyTextStorage): + case let .anyTextStorage: var resolved = Text.ResolvedString() resolved.idiom = idiom - storage.resolve(into: &resolved, in: environment, with: options) + resolve(into: &resolved, in: environment, with: options) return resolved.string } } @@ -260,15 +268,15 @@ public struct Text: Equatable, Sendable { in environment: EnvironmentValues, with options: Text.ResolveOptions ) where T: ResolvedTextContainer { - let style = result.style + let oldStyle = result.style if modifiers.isEmpty { storage.resolve(into: &result, in: environment, with: options) } else { - for modifier in modifiers { + for modifier in modifiers.reversed() { modifier.modify(style: &result.style, environment: environment) } - result.style = style storage.resolve(into: &result, in: environment, with: options) + result.style = oldStyle } } From 5f883e355d3cc0ee4ff935b41897da70c0660640 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 22:12:00 +0800 Subject: [PATCH 04/24] Update ResolvedText --- .../View/Text/Resolve/ResolvedText.swift | 190 ++++++++++-------- .../View/Text/Text/TextNonDarwinShims.swift | 5 +- .../Shims/UIFoundation/NSTextAttachment.h | 29 +++ 3 files changed, 141 insertions(+), 83 deletions(-) create mode 100644 Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 431372988..dfe2aacc4 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -84,19 +84,19 @@ extension ResolvedTextContainer { extension Text { package struct Resolved: ResolvedTextContainer { package var style: Text.Style = .init() - + package var attributedString: NSMutableAttributedString? - + package var includeDefaultAttributes: Bool = true - + package var idiom: AnyInterfaceIdiom? - + package var properties: Text.ResolvedProperties = .init() - + package init() { _openSwiftUIEmptyStub() } - + package mutating func append( _ string: S, in env: EnvironmentValues, @@ -117,7 +117,7 @@ extension Text { } _openSwiftUIUnimplementedWarning() } - + package mutating func append( _ attributedString: NSAttributedString, in env: EnvironmentValues, @@ -132,7 +132,7 @@ extension Text { } _openSwiftUIUnimplementedWarning() } - + package mutating func append( _ image: Image.Resolved, in environment: EnvironmentValues, @@ -140,7 +140,7 @@ extension Text { ) { _openSwiftUIUnimplementedFailure() } - + package mutating func append( _ namedImage: Image.NamedResolved, in environment: EnvironmentValues, @@ -148,7 +148,7 @@ extension Text { ) { _openSwiftUIUnimplementedFailure() } - + package mutating func append( resolvable: R, in environment: EnvironmentValues, @@ -157,7 +157,7 @@ extension Text { ) where R: ResolvableStringAttribute { _openSwiftUIUnimplementedFailure() } - + package func nsAttributes( content: (() -> String)?, in environment: EnvironmentValues, @@ -166,7 +166,7 @@ extension Text { ) -> [NSAttributedString.Key: Any] { _openSwiftUIUnimplementedFailure() } - + private mutating func append( _ string: String, with attributes: [NSAttributedString.Key: Any], @@ -194,9 +194,9 @@ extension Text { } } } - + // MARK: - Text.Style [WIP] - + package struct Style { internal var baseFont: TextStyleFont = .default internal var fontModifiers: [AnyFontModifier] = [] @@ -210,32 +210,32 @@ extension Text { internal var encapsulation: Text.Encapsulation? internal var speech: AccessibilitySpeechAttributes? package var accessibility: AccessibilityTextAttributes? - #if canImport(CoreText) +#if canImport(CoreText) internal var glyphInfo: CTGlyphInfo? - #endif -// internal var shadow: TextShadowModifier? -// internal var transition: TextTransitionModifier? +#endif + // internal var shadow: TextShadowModifier? + // internal var transition: TextTransitionModifier? internal var scale: Text.Scale? internal var superscript: Text.Superscript? internal var typesettingConfiguration: TypesettingConfiguration = .init() internal var customAttributes: [TextAttributeModifierBase] = [] - #if canImport(Darwin) +#if canImport(Darwin) internal var adaptiveImageGlyph: AttributedString.AdaptiveImageGlyph? - #endif +#endif package var clearedFontModifiers: Set = [] - + init() { // FIXME _openSwiftUIUnimplementedWarning() } - + // 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? @@ -258,15 +258,15 @@ extension Text { ) } } - + // 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, @@ -316,14 +316,14 @@ extension Text { } } } - + // MARK: - Text.Style.TextStyleFont - + package enum TextStyleFont { case implicit case explicit(Font) case `default` - + package func resolve( in environment: EnvironmentValues, includeDefaultAttributes: Bool = true @@ -341,122 +341,164 @@ extension Text { 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() } } - + // FIXME package struct ResolvedProperties { package var insets: EdgeInsets = .zero - + package var features: Text.ResolvedProperties.Features = [] - + package var styles: [_ShapeStyle_Pack.Style] = [] - + package var transitions: [Text.ResolvedProperties.Transition] = [] - + package var suffix: ResolvedTextSuffix = .none - + package struct CustomAttachments { package var characterIndices: [Int] - + package init(characterIndices: [Int] = []) { self.characterIndices = characterIndices } - + package var isEmpty: Bool { characterIndices.isEmpty } } - + package var customAttachments: Text.ResolvedProperties.CustomAttachments = .init() - + package init() { _openSwiftUIUnimplementedWarning() } - + package mutating func registerCustomAttachment(at offset: Int) { _openSwiftUIUnimplementedFailure() } - + package struct Features: OptionSet { package let rawValue: UInt16 - + package init(rawValue: UInt16) { self.rawValue = rawValue } - + package static let keyColor: Text.ResolvedProperties.Features = .init(rawValue: 1 << 0) - + package static let attachments: Text.ResolvedProperties.Features = .init(rawValue: 1 << 1) - + package static let sensitive: Text.ResolvedProperties.Features = .init(rawValue: 1 << 2) - + package static let customRenderer: Text.ResolvedProperties.Features = .init(rawValue: 1 << 3) - + package static let useTextLayoutManager: Text.ResolvedProperties.Features = .init(rawValue: 1 << 4) - + package static let useTextSuffix: Text.ResolvedProperties.Features = .init(rawValue: 1 << 5) - + package static let produceTextLayout: Text.ResolvedProperties.Features = .init(rawValue: 1 << 6) - + package static let checkInterpolationStrategy: Text.ResolvedProperties.Features = .init(rawValue: 1 << 8) - + package static let isUniqueSizeVariant: Text.ResolvedProperties.Features = .init(rawValue: 1 << 8) } - + package struct Transition: Equatable { package var transition: ContentTransition - + package init(transition: ContentTransition) { self.transition = transition } } - + package struct Paragraph { package var compositionLanguage: NSCompositionLanguage - + var cachedStyle: NSParagraphStyle? } - + package var paragraph: Text.ResolvedProperties.Paragraph = .init(compositionLanguage: .none) - + package mutating func addColor(_ c: Color.Resolved) { _openSwiftUIUnimplementedFailure() } - + package mutating func addAttachment() { _openSwiftUIUnimplementedFailure() } - + package mutating func addSensitive() { features.insert(.sensitive) } - + package mutating func addCustomStyle(_ style: _ShapeStyle_Pack.Style) -> Color.Resolved { _openSwiftUIUnimplementedFailure() } } } +// MARK: - EnvironmentValues + disableLinkColor + +extension EnvironmentValues { + private struct DisableLinkColorKey: EnvironmentKey { + static var defaultValue: Bool { false } + } + + package var disableLinkColor: Bool { + get { self[DisableLinkColorKey.self] } + set { self[DisableLinkColorKey.self] = newValue } + } +} + +// MARK: - EnvironmentValues + resolvedTextProvider + +package protocol ResolvedTextProvider { + static func defaultLinkColor(for environment: EnvironmentValues) -> Color + + static func updateImageTextAttachment( + in attachment: NSTextAttachment, + image: Image.Resolved + ) + + static func updateWidgetTextAttachment( + _ attachment: NSTextAttachment, + namedImage: Image.NamedResolved + ) +} + +extension EnvironmentValues { + private struct ResolvedTextProviderKey: EnvironmentKey { + static var defaultValue: (any ResolvedTextProvider.Type)? + } + + package var resolvedTextProvider: (any ResolvedTextProvider.Type)? { + get { self[ResolvedTextProviderKey.self] } + set { self[ResolvedTextProviderKey.self] = newValue } + } +} + +// MARK: - Text.ResolvedString + extension Text { struct ResolvedString: ResolvedTextContainer { var style: Text.Style = .init() @@ -464,10 +506,6 @@ extension Text { var string: String = "" var hasResolvableAttributes: Bool = false - init() { - _openSwiftUIEmptyStub() - } - mutating func append( _ string: S, in env: EnvironmentValues, @@ -526,6 +564,7 @@ extension Text { Log.internalWarning("Unable to resolve custom attribute \(resolvable)") return } + hasResolvableAttributes = true append( String(attributedString.characters), in: environment, @@ -534,14 +573,3 @@ extension Text { } } } - -extension EnvironmentValues { - private struct DisableLinkColorKey: EnvironmentKey { - static var defaultValue: Bool { false } - } - - package var disableLinkColor: Bool { - get { self[DisableLinkColorKey.self] } - set { self[DisableLinkColorKey.self] = newValue } - } -} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift index 7efddf577..45f35884a 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift @@ -6,8 +6,9 @@ public import Foundation public import UIFoundation_Private -package class NSParagraphStyle {} -package class NSMutableParagraphStyle {} +package class NSParagraphStyle: NSObject {} +package class NSMutableParagraphStyle: NSObject {} +package class TextAttachment: NSObject {} extension NSMutableAttributedString { package var isEmptyOrTerminatedByParagraphSeparator: Bool { diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h new file mode 100644 index 000000000..7948c2b87 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h @@ -0,0 +1,29 @@ +// +// NSTextAttachment.h +// OpenSwiftUI_SPI + +#pragma once + +#import "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +// Modified based on macOS 27.0 SDK and iOS 18.5 SDK + +#import +#import + +NS_HEADER_AUDIT_BEGIN(nullability, sendability) + +OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)) +@interface NSTextAttachment : NSObject + +@property (nullable, copy) NSData *contents; +@property (nullable, copy) NSString *fileType; +@property CGRect bounds; + +@end + +NS_HEADER_AUDIT_END(nullability, sendability) + +#endif \ No newline at end of file From 52c8a91abddc721cc3fc28803284fa6b995d327c Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 22:20:49 +0800 Subject: [PATCH 05/24] Add empty ResolvableTimer --- .../View/Text/Resolve/ResolvableTimer.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTimer.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTimer.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTimer.swift new file mode 100644 index 000000000..15bbcdb27 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTimer.swift @@ -0,0 +1,10 @@ +// +// ResolvableTimer.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Empty +// ID: 5596C2D3913FECC4138CDA24E2471B2B (SwiftUICore) + +// FIXME +package struct ResolvableTimer {} From 7cb95c0571f60f90df09f474e5305ad486e29a7f Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 22:21:58 +0800 Subject: [PATCH 06/24] Update TextNonDarwinShims --- Sources/OpenSwiftUICore/View/Text/Text/Text.swift | 4 ---- .../OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index 204a7e6a9..8975d9061 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -8,10 +8,6 @@ public import Foundation -#if !canImport(ObjectiveC) -public typealias NSInteger = Int -#endif - // MARK: - Text @available(OpenSwiftUI_v1_0, *) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift index 45f35884a..5af584035 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift @@ -6,6 +6,8 @@ public import Foundation public import UIFoundation_Private +public typealias NSInteger = Int + package class NSParagraphStyle: NSObject {} package class NSMutableParagraphStyle: NSObject {} package class TextAttachment: NSObject {} From 0730ab5e42b55fb5418d1a6a6e7526e26504fb6b Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 22:24:11 +0800 Subject: [PATCH 07/24] Update _GraphInputs.defaultInterfaceIdiom --- .../InterfaceIdiom/InterfaceIdiomPredicate.swift | 2 +- Sources/OpenSwiftUICore/View/Text/Text/Text.swift | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/InterfaceIdiom/InterfaceIdiomPredicate.swift b/Sources/OpenSwiftUICore/View/InterfaceIdiom/InterfaceIdiomPredicate.swift index ca08c588a..f717ba869 100644 --- a/Sources/OpenSwiftUICore/View/InterfaceIdiom/InterfaceIdiomPredicate.swift +++ b/Sources/OpenSwiftUICore/View/InterfaceIdiom/InterfaceIdiomPredicate.swift @@ -33,7 +33,7 @@ extension _GraphInputs { #elseif os(iOS) || os(visionOS) AnyInterfaceIdiom(.phone) #else - _openSwiftUIUnimplementedFailure() + AnyInterfaceIdiom(.nokit) #endif } } diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index 8975d9061..782f8f5a2 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -333,11 +333,7 @@ package class AnyTextStorage { with options: Text.ResolveOptions ) -> Bool { var resolved = Text.ResolvedString() - #if os(iOS) || os(visionOS) - resolved.idiom = .init(.phone) - #elseif os(macOS) - resolved.idiom = isCatalyst() ? .init(.pad) : .init(.mac) - #endif + resolved.idiom = _GraphInputs.defaultInterfaceIdiom resolve(into: &resolved, in: environment, with: options) return resolved.string.isEmpty } @@ -368,11 +364,7 @@ extension AnyTextStorage: CustomDebugStringConvertible { package var debugDescription: String { var description = "<\(Self.self): \(self)>" var resolved = Text.Resolved() - #if os(iOS) || os(visionOS) - resolved.idiom = .init(.phone) - #elseif os(macOS) - resolved.idiom = isCatalyst() ? .init(.pad) : .init(.mac) - #endif + resolved.idiom = _GraphInputs.defaultInterfaceIdiom resolve(into: &resolved, in: .init(), with: []) if let attributedString = resolved.attributedString { description.append(#": "\#(attributedString.string)""#) From e5be4fe2e40eabf356e8d40947402dbaa9d5570b Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 12 Jul 2026 22:31:58 +0800 Subject: [PATCH 08/24] Update AnyTextModifier --- .../OpenSwiftUICore/View/Text/Text/Text.swift | 360 +++++++++++++++--- .../View/Text/Text/TextUnimplemented.swift | 8 - 2 files changed, 315 insertions(+), 53 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index 782f8f5a2..ed376c77f 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 7800CE2E251A218329C9998E1C3194FD (SwiftUICore) public import Foundation @@ -373,7 +373,7 @@ extension AnyTextStorage: CustomDebugStringConvertible { } } -// MARK: - AnyTextModifier [WIP] +// MARK: - AnyTextModifier @available(OpenSwiftUI_v1_0, *) @usableFromInline @@ -394,45 +394,310 @@ package class AnyTextModifier { @available(*, unavailable) extension AnyTextModifier: Sendable {} -//final package class SpeechModifier: AnyTextModifier { -// final package let value: AccessibilitySpeechAttributes -// -// package init(_ value: AccessibilitySpeechAttributes) { -// _openSwiftUIUnimplementedFailure() -// } -// -// final package func isStyled(options: Text.ResolveOptions = []) -> Bool { -// _openSwiftUIUnimplementedFailure() -// } -// -// final package func modify(style: inout Text.Style, environment: EnvironmentValues) { -// _openSwiftUIUnimplementedFailure() -// } -// -// final package func isEqual(to other: AnyTextModifier) -> Bool { -// _openSwiftUIUnimplementedFailure() -// } -//} -// -//final package class TextShadowModifier: AnyTextModifier { -// final package func modify(style: inout Text.Style, environment: EnvironmentValues) { -// _openSwiftUIUnimplementedFailure() -// } -// -// final package func isEqual(to other: AnyTextModifier) -> Bool { -// _openSwiftUIUnimplementedFailure() -// } -//} -// -//final package class TextTransitionModifier: AnyTextModifier { -// final package func modify(style: inout Text.Style, environment: EnvironmentValues) { -// _openSwiftUIUnimplementedFailure() -// } -// -// final package func isEqual(to other: AnyTextModifier) -> Bool { -// _openSwiftUIUnimplementedFailure() -// } -//} +final class StrikethroughTextModifier: AnyTextModifier { + let lineStyle: Text.LineStyle? + + init(_ lineStyle: Text.LineStyle?) { + self.lineStyle = lineStyle + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.strikethrough = lineStyle.map { .explicit($0) } ?? .default + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? StrikethroughTextModifier else { + return false + } + return lineStyle == other.lineStyle + } +} + +final class UnderlineTextModifier: AnyTextModifier { + let lineStyle: Text.LineStyle? + + init(_ lineStyle: Text.LineStyle?) { + self.lineStyle = lineStyle + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.underline = lineStyle.map { .explicit($0) } ?? .default + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? UnderlineTextModifier else { + return false + } + return lineStyle == other.lineStyle + } +} + +private final class StylisticAlternativeTextModifier: AnyTextModifier { + let value: Font._StylisticAlternative + + init(_ value: Font._StylisticAlternative) { + self.value = value + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.addFontModifier(Font.StylisticAlternativeModifier(alternative: value)) + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? StylisticAlternativeTextModifier else { + return false + } + return value == other.value + } +} + +final class BoldTextModifier: AnyTextModifier { + let isActive: Bool + + init(isActive: Bool = true) { + self.isActive = isActive + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + if isActive { + style.addFontModifier(type: Font.BoldModifier.self) + } else { + style.removeFontModifier(ofType: Font.BoldModifier.self) + } + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? BoldTextModifier else { + return false + } + return isActive == other.isActive + } +} + +final class ItalicTextModifier: AnyTextModifier { + let isActive: Bool + + init(isActive: Bool = true) { + self.isActive = isActive + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + if isActive { + style.addFontModifier(type: Font.ItalicModifier.self) + } else { + style.removeFontModifier(ofType: Font.ItalicModifier.self) + } + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? ItalicTextModifier else { + return false + } + return isActive == other.isActive + } +} + +final class MonospacedTextModifier: AnyTextModifier { + let isActive: Bool + + init(isActive: Bool = true) { + self.isActive = isActive + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + if isActive { + style.addFontModifier(type: Font.MonospacedModifier.self) + } else { + style.removeFontModifier(ofType: Font.MonospacedModifier.self) + } + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? MonospacedTextModifier else { + return false + } + return isActive == other.isActive + } +} + +final class TextDesignModifier: AnyTextModifier { + let design: Font.Design? + + init(_ design: Font.Design?) { + self.design = design + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + if let design { + style.addFontModifier(Font.DesignModifier(design: design)) + } else { + style.removeFontModifier(ofType: Font.DesignModifier.self) + } + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? TextDesignModifier else { + return false + } + return design == other.design + } +} + +final class MonospacedDigitTextModifier: AnyTextModifier { + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.addFontModifier(type: Font.MonospacedDigitModifier.self) + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + other is MonospacedDigitTextModifier + } +} + +final class CollapsibleTextModifier: AnyTextModifier { + override func isStyled(options: Text.ResolveOptions) -> Bool { + false + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) {} + + override func isEqual(to other: AnyTextModifier) -> Bool { + other is CollapsibleTextModifier + } +} + +final package class SpeechModifier: AnyTextModifier { + let value: AccessibilitySpeechAttributes + + init(_ value: AccessibilitySpeechAttributes) { + self.value = value + } + + override package func isStyled(options: Text.ResolveOptions) -> Bool { + options.contains(.includeAccessibility) + } + + override package func modify(style: inout Text.Style, environment: EnvironmentValues) { + if let speech = style.speech { + style.speech = value.combined(with: speech) + } else { + style.speech = value + } + } + + override package func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? SpeechModifier else { + return false + } + return value == other.value + } +} + +final package class TextShadowModifier: AnyTextModifier { + let shadow: _ShadowEffect + + init(_ shadow: _ShadowEffect) { + self.shadow = shadow + } + + override package func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.shadow = self + } + + override package func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? TextShadowModifier else { + return false + } + return shadow == other.shadow + } +} + +final package class TextTransitionModifier: AnyTextModifier { + let resolved: Text.ResolvedProperties.Transition + + init(_ transition: ContentTransition) { + self.resolved = .init(transition: transition) + } + + override package func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.transition = self + } + + override package func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? TextTransitionModifier else { + return false + } + return resolved == other.resolved + } +} + +final class TextWidthModifier: AnyTextModifier { + let width: CGFloat? + + init(_ width: CGFloat?) { + self.width = width + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + if let width { + style.addFontModifier(Font.WidthModifier(width: width)) + } else { + style.removeFontModifier(ofType: Font.WidthModifier.self) + } + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? TextWidthModifier else { + return false + } + return width == other.width + } +} + +final class TextForegroundStyleModifier: AnyTextModifier { + let style: AnyShapeStyle + + init(_ style: S) where S: ShapeStyle { + self.style = AnyShapeStyle(style) + } + + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + let foregroundStyle = style.color.baseStyle(in: environment) + let newStyle = self.style.copyStyle(in: environment, foregroundStyle: foregroundStyle) + style.color = .explicit(newStyle) + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + guard let other = other as? TextForegroundStyleModifier else { + return false + } + return style.storage == other.style.storage + } +} + +final class TextForegroundKeyColorModifier: AnyTextModifier { + override func modify(style: inout Text.Style, environment: EnvironmentValues) { + style.color = .foregroundKeyColor(base: style.color.baseStyle(in: environment)) + } + + override func isEqual(to other: AnyTextModifier) -> Bool { + other is TextForegroundKeyColorModifier + } +} + +extension Text.Style.TextStyleColor { + fileprivate func baseStyle(in environment: EnvironmentValues) -> AnyShapeStyle { + switch self { + case .implicit: + return environment.foregroundStyle ?? HierarchicalShapeStyle.sharedPrimary + case let .explicit(anyShapeStyle): + return anyShapeStyle + case .default: + return environment.defaultForegroundStyle ?? HierarchicalShapeStyle.sharedPrimary + case let .foregroundKeyColor(base): + return base + } + } +} @available(OpenSwiftUI_v2_0, *) extension Text { @@ -465,25 +730,30 @@ extension Text { } } +// MARK: - Text + shadow + @_spi(_) @available(OpenSwiftUI_v3_0, *) extension Text { - @_spi(_) public func shadow( color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), radius: CGFloat, x: CGFloat = 0, y: CGFloat = 0 ) -> Text { - _openSwiftUIUnimplementedFailure() + modified(with: .anyTextModifier(TextShadowModifier( + _ShadowEffect(color: color, radius: radius, offset: CGSize(width: x, height: y)) + ))) } } -@_spi(OpenSwiftUIPrivate) +// MARK: - Text + contentTransition + +@_spi(Private) @available(OpenSwiftUI_v4_0, *) extension Text { public func contentTransition(_ transition: ContentTransition) -> Text { - _openSwiftUIUnimplementedFailure() + modified(with: .anyTextModifier(TextTransitionModifier(transition))) } } diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift index 9a55d77e1..443f2b43b 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift @@ -10,11 +10,3 @@ extension Text { self } } - -class BoldTextModifier: AnyTextModifier { - -} - -class MonospacedTextModifier: AnyTextModifier { - -} From df924482cfebc449204e4484f3b7b6c900ec95ca Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Jul 2026 00:23:18 +0800 Subject: [PATCH 09/24] Add docc documentation for Text.swift --- .../OpenSwiftUICore/View/Text/Text/Text.swift | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index ed376c77f..bb190788a 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -10,6 +10,141 @@ public import Foundation // MARK: - Text +/// A view that displays one or more lines of read-only text. +/// +/// A text view draws a string in your app's user interface using a +/// ``Font/body`` font that's appropriate for the current platform. You can +/// choose a different standard font, like ``Font/title`` or ``Font/caption``, +/// using the ``View/font(_:)`` view modifier. +/// +/// Text("Hamlet") +/// .font(.title) +/// +/// ![A text view showing the name "Hamlet" in a title +/// font.](OpenSwiftUI-Text-title.png) +/// +/// If you need finer control over the styling of the text, you can use the same +/// modifier to configure a system font or choose a custom font. You can also +/// apply view modifiers like ``Text/bold()`` or ``Text/italic()`` to further +/// adjust the formatting. +/// +/// Text("by William Shakespeare") +/// .font(.system(size: 12, weight: .light, design: .serif)) +/// .italic() +/// +/// ![A text view showing by William Shakespeare in a 12 point, light, italic, +/// serif font.](OpenSwiftUI-Text-font.png) +/// +/// To apply styling within specific portions of the text, you can create +/// the text view from an +/// [AttributedString](https://developer.apple.com/documentation/foundation/attributedstring), +/// which in turn allows you to use Markdown to style runs of text. You can +/// mix string attributes and OpenSwiftUI modifiers, with the string attributes +/// taking priority. +/// +/// let attributedString = try! AttributedString( +/// markdown: "_Hamlet_ by William Shakespeare") +/// +/// var body: some View { +/// Text(attributedString) +/// .font(.system(size: 12, weight: .light, design: .serif)) +/// } +/// +/// ![A text view showing Hamlet by William Shakespeare in a 12 point, light, +/// serif font, with the title Hamlet in italics.](OpenSwiftUI-Text-attributed.png) +/// +/// A text view always uses exactly the amount of space it needs to display its +/// rendered contents, but you can affect the view's layout. For example, you +/// can use the ``View/frame(width:height:alignment:)`` modifier to propose +/// specific dimensions to the view. If the view accepts the proposal but the +/// text doesn't fit into the available space, the view uses a combination of +/// wrapping, tightening, scaling, and truncation to make it fit. With a width +/// of `100` points but no constraint on the height, a text view might wrap a +/// long string: +/// +/// Text("To be, or not to be, that is the question:") +/// .frame(width: 100) +/// +/// ![A text view showing a quote from Hamlet split over three +/// lines.](OpenSwiftUI-Text-split.png) +/// +/// Use modifiers like ``View/lineLimit(_:)``, ``View/allowsTightening(_:)``, +/// ``View/minimumScaleFactor(_:)``, and ``View/truncationMode(_:)`` to +/// configure how the view handles space constraints. For example, combining a +/// fixed width and a line limit of `1` results in truncation for text that +/// doesn't fit in that space: +/// +/// Text("Brevity is the soul of wit.") +/// .frame(width: 100) +/// .lineLimit(1) +/// +/// ![A text view showing a truncated quote from Hamlet starting Brevity is t +/// and ending with three dots.](OpenSwiftUI-Text-truncated.png) +/// +/// ### Localizing strings +/// +/// If you initialize a text view with a string literal, the view uses the +/// ``Text/init(_:tableName:bundle:comment:)`` initializer, which interprets the +/// string as a localization key and searches for the key in the table you +/// specify, or in the default table if you don't specify one. +/// +/// Text("pencil") // Searches the default table in the main bundle. +/// +/// For an app localized in both English and Spanish, the above view displays +/// "pencil" and "lápiz" for English and Spanish users, respectively. If the +/// view can't perform localization, it displays the key instead. For example, +/// if the same app lacks Danish localization, the view displays "pencil" for +/// users in that locale. Similarly, an app that lacks any localization +/// information displays "pencil" in any locale. +/// +/// To explicitly bypass localization for a string literal, use the +/// ``Text/init(verbatim:)`` initializer. +/// +/// Text(verbatim: "pencil") // Displays the string "pencil" in any locale. +/// +/// If you initialize a text view with a variable value, the view uses the +/// ``Text/init(_:)-9d1g4`` initializer, which doesn't localize the string. However, +/// you can request localization by creating a ``LocalizedStringKey`` instance +/// first, which triggers the ``Text/init(_:tableName:bundle:comment:)`` +/// initializer instead: +/// +/// // Don't localize a string variable... +/// Text(writingImplement) +/// +/// // ...unless you explicitly convert it to a localized string key. +/// Text(LocalizedStringKey(writingImplement)) +/// +/// When localizing a string variable, you can use the default table by omitting +/// the optional initialization parameters — as in the above example — just like +/// you might for a string literal. +/// +/// When composing a complex string, where there is a need to assemble multiple +/// pieces of text, use string interpolation: +/// +/// let name: String = //… +/// Text("Hello, \(name)") +/// +/// This would look up the `"Hello, %@"` localization key in the localized +/// string file and replace the format specifier `%@` with the value of `name` +/// before rendering the text on screen. +/// +/// Using string interpolation ensures that the text in your app can be localized +/// correctly in all locales, especially in right-to-left languages. +/// +/// If you desire to style only parts of interpolated text while ensuring that +/// the content can still be localized correctly, interpolate `Text` or +/// [AttributedString](https://developer.apple.com/documentation/foundation/attributedstring): +/// +/// let name = Text(person.name).bold() +/// Text("Hello, \(name)") +/// +/// The example above uses ``LocalizedStringKey/StringInterpolation/appendInterpolation(_:)-4qyfo`` +/// and will look up the `"Hello, %@"` in the localized string file and +/// interpolate a bold text rendering the value of `name`. +/// +/// Using ``LocalizedStringKey/StringInterpolation/appendInterpolation(_:)-5m52e`` +/// you can interpolate ``Image`` in text. +/// @available(OpenSwiftUI_v1_0, *) @frozen public struct Text: Equatable, Sendable { @@ -211,11 +346,43 @@ public struct Text: Equatable, Sendable { @usableFromInline package var modifiers: [Text.Modifier] = [Modifier]() + /// Creates a text view that displays a string literal without localization. + /// + /// Use this initializer to create a text view with a string literal without + /// performing localization: + /// + /// Text(verbatim: "pencil") // Displays the string "pencil" in any locale. + /// + /// If you want to localize a string literal before displaying it, use the + /// ``Text/init(_:tableName:bundle:comment:)`` initializer instead. If you + /// want to display a string variable, use the ``Text/init(_:)-9d1g4`` + /// initializer, which also bypasses localization. + /// + /// - Parameter content: A string to display without localization. @inlinable public init(verbatim content: String) { storage = .verbatim(content) } + /// Creates a text view that displays a stored string without localization. + /// + /// Use this initializer to create a text view that displays — without + /// localization — the text in a string variable. + /// + /// Text(someString) // Displays the contents of `someString` without localization. + /// + /// OpenSwiftUI doesn't call the `init(_:)` method when you initialize a text + /// view with a string literal as the input. Instead, a string literal + /// triggers the ``Text/init(_:tableName:bundle:comment:)`` method — which + /// treats the input as a ``LocalizedStringKey`` instance — and attempts to + /// perform localization. + /// + /// By default, OpenSwiftUI assumes that you don't want to localize stored + /// strings, but if you do, you can first create a localized string key from + /// the value, and initialize the text view with that. Using a key as input + /// triggers the ``Text/init(_:tableName:bundle:comment:)`` method instead. + /// + /// - Parameter content: The string value to display without localization. @_disfavoredOverload public init(_ content: S) where S: StringProtocol { storage = .verbatim(String(content)) From 64df187910f09637abaefc63c3f1e0be609e77fe Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Jul 2026 00:57:51 +0800 Subject: [PATCH 10/24] Fix docc id --- .../View/Text/Localized/Text+Localized.swift | 2 +- Sources/OpenSwiftUICore/View/Text/Text/Text.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Localized/Text+Localized.swift b/Sources/OpenSwiftUICore/View/Text/Localized/Text+Localized.swift index 749b97c09..dc85aa0a6 100644 --- a/Sources/OpenSwiftUICore/View/Text/Localized/Text+Localized.swift +++ b/Sources/OpenSwiftUICore/View/Text/Localized/Text+Localized.swift @@ -34,7 +34,7 @@ extension Text { /// typically contains the unlocalized string. /// /// If you initialize a text view with a string variable rather than a - /// string literal, the view triggers the ``Text/init(_:)-9d1g4`` + /// string literal, the view triggers the ``Text/init(_:)-3dz13`` /// initializer instead, because it assumes that you don't want localization /// in that case. If you do want to localize the value stored in a string /// variable, you can choose to call the `init(_:tableName:bundle:comment:)` diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index bb190788a..a2c40da3b 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -103,7 +103,7 @@ public import Foundation /// Text(verbatim: "pencil") // Displays the string "pencil" in any locale. /// /// If you initialize a text view with a variable value, the view uses the -/// ``Text/init(_:)-9d1g4`` initializer, which doesn't localize the string. However, +/// ``Text/init(_:)-3dz13`` initializer, which doesn't localize the string. However, /// you can request localization by creating a ``LocalizedStringKey`` instance /// first, which triggers the ``Text/init(_:tableName:bundle:comment:)`` /// initializer instead: @@ -138,7 +138,7 @@ public import Foundation /// let name = Text(person.name).bold() /// Text("Hello, \(name)") /// -/// The example above uses ``LocalizedStringKey/StringInterpolation/appendInterpolation(_:)-4qyfo`` +/// The example above uses ``LocalizedStringKey/StringInterpolation/appendInterpolation(_:)-7xnfe`` /// and will look up the `"Hello, %@"` in the localized string file and /// interpolate a bold text rendering the value of `name`. /// @@ -355,7 +355,7 @@ public struct Text: Equatable, Sendable { /// /// If you want to localize a string literal before displaying it, use the /// ``Text/init(_:tableName:bundle:comment:)`` initializer instead. If you - /// want to display a string variable, use the ``Text/init(_:)-9d1g4`` + /// want to display a string variable, use the ``Text/init(_:)-3dz13`` /// initializer, which also bypasses localization. /// /// - Parameter content: A string to display without localization. From e314f12524942045b75ce3c97017d908fe897512 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Jul 2026 00:58:10 +0800 Subject: [PATCH 11/24] Add Text+modifier --- .../View/Text/Text/Text+Renderer.swift | 150 +++++++++++++++++- .../OpenSwiftUICore/View/Text/Text/Text.swift | 62 ++++++++ .../View/Text/Text/TextUnimplemented.swift | 12 -- 3 files changed, 210 insertions(+), 14 deletions(-) delete mode 100644 Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift index 8229d3bc2..b34b13440 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift @@ -53,7 +53,7 @@ extension Text { /// /// - Returns: A version of the text view with `value` attached. public func customAttribute(_ value: T) -> Text where T: TextAttribute { - modified(with: .anyTextModifier(TextAttributeModifier(value: value))) + modified(with: .customAttribute(value)) } } @@ -94,6 +94,13 @@ private final class TextAttributeModifier: TextAttributeModifierBase wher } } +extension Text.Modifier { + @inline(__always) + static func customAttribute(_ value: T) -> Self where T: TextAttribute { + .anyTextModifier(TextAttributeModifier(value: value)) + } +} + // MARK: Text + CustomAttributes @_spi(Private) @@ -546,7 +553,10 @@ extension Text.Layout { } } -// TODO +// TODO: GraphicsContext + draw for Text.Layout + +// TODO: Text.Layout + foregroundColor + // MARK: - TextRendererInput @@ -575,3 +585,139 @@ extension EnvironmentValues { set { self[TextRendererAddsDrawingGroupKey.self] = newValue } } } + +// MARK: - Text + modifier + +@available(OpenSwiftUI_v1_0, *) +extension Text { + @available(*, deprecated, renamed: "foregroundStyle(_:)") + public func foregroundColor(_ color: Color?) -> Text { + modified(with: .color(color)) + } + + @available(OpenSwiftUI_v5_0, *) + public func foregroundStyle(_ style: S) -> Text where S: ShapeStyle { + if let color = style as? Color { + return modified(with: .color(color)) + } else { + return modified(with: .foregroundStyle(style)) + } + } + + public func font(_ font: Font?) -> Text { + modified(with: .font(font)) + } + + public func fontWeight(_ weight: Font.Weight?) -> Text { + modified(with: .weight(weight)) + } + + @available(OpenSwiftUI_v4_0, *) + public func fontWidth(_ width: Font.Width?) -> Text { + modified(with: .width(width?.value)) + } + + public func bold() -> Text { + modified(with: .bold()) + } + + @available(OpenSwiftUI_v4_0, *) + public func bold(_ isActive: Bool) -> Text { + modified(with: .bold(isActive)) + } + + public func italic() -> Text { + modified(with: .italic()) + } + + @available(OpenSwiftUI_v4_0, *) + public func italic(_ isActive: Bool) -> Text { + modified(with: .italic(isActive)) + } + + @available(OpenSwiftUI_v4_4, *) + public func monospaced(_ isActive: Bool = true) -> Text { + modified(with: .monospaced(isActive)) + } + + @available(OpenSwiftUI_v4_1, *) + public func fontDesign(_ design: Font.Design?) -> Text { + modified(with: .design(design)) + } + + @available(OpenSwiftUI_v3_0, *) + public func monospacedDigit() -> Text { + modified(with: .monospacedDigit()) + } + + public func strikethrough( + _ isActive: Bool = true, + color: Color? = nil + ) -> Text { + modified(with: .strikethrough( + isActive ? Text.LineStyle(color: color) : nil + )) + } + + @available(OpenSwiftUI_v4_0, *) + public func strikethrough( + _ isActive: Bool = true, + pattern: Text.LineStyle.Pattern, + color: Color? = nil + ) -> Text { + modified(with: .strikethrough( + isActive ? Text.LineStyle(pattern: pattern, color: color) : nil + )) + } + + public func underline( + _ isActive: Bool = true, + color: Color? = nil + ) -> Text { + modified(with: .underline( + isActive ? Text.LineStyle(color: color) : nil + )) + } + + @available(OpenSwiftUI_v4_0, *) + public func underline( + _ isActive: Bool = true, + pattern: Text.LineStyle.Pattern, + color: Color? = nil + ) -> Text { + modified(with: .underline( + isActive ? Text.LineStyle(pattern: pattern, color: color) : nil + )) + } + + public func kerning(_ kerning: CGFloat) -> Text { + modified(with: .kerning(kerning)) + } + + public func tracking(_ tracking: CGFloat) -> Text { + modified(with: .tracking(tracking)) + } + + public func baselineOffset(_ baselineOffset: CGFloat) -> Text { + modified(with: .baseline(baselineOffset)) + } + + public func _stylisticAlternative(_ alternative: Font._StylisticAlternative) -> Text { + modified(with: .stylisticAlternative(alternative)) + } + + @_spi(Private) + @available(OpenSwiftUI_v3_0, *) + public func collapsible() -> Text { + modified(with: .collapsible()) + } + + package func isCollapsible() -> Bool { + modifiers.contains { + guard case let .anyTextModifier(modifier) = $0 else { + return false + } + return modifier is CollapsibleTextModifier + } + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index a2c40da3b..013419cd1 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -561,6 +561,68 @@ package class AnyTextModifier { @available(*, unavailable) extension AnyTextModifier: Sendable {} +extension Text.Modifier { + @inline(__always) + static func strikethrough(_ lineStyle: Text.LineStyle?) -> Self { + .anyTextModifier(StrikethroughTextModifier(lineStyle)) + } + + @inline(__always) + static func underline(_ lineStyle: Text.LineStyle?) -> Self { + .anyTextModifier(UnderlineTextModifier(lineStyle)) + } + + @inline(__always) + static func stylisticAlternative(_ alternative: Font._StylisticAlternative) -> Self { + .anyTextModifier(StylisticAlternativeTextModifier(alternative)) + } + + @inline(__always) + static func bold(_ isActive: Bool = true) -> Self { + .anyTextModifier(BoldTextModifier(isActive: isActive)) + } + + @inline(__always) + static func italic(_ isActive: Bool = true) -> Self { + .anyTextModifier(ItalicTextModifier(isActive: isActive)) + } + + @inline(__always) + static func monospaced(_ isActive: Bool = true) -> Self { + .anyTextModifier(MonospacedTextModifier(isActive: isActive)) + } + + @inline(__always) + static func design(_ design: Font.Design?) -> Self { + .anyTextModifier(TextDesignModifier(design)) + } + + @inline(__always) + static func monospacedDigit() -> Self { + .anyTextModifier(MonospacedDigitTextModifier()) + } + + @inline(__always) + static func collapsible() -> Self { + .anyTextModifier(CollapsibleTextModifier()) + } + + @inline(__always) + static func width(_ width: CGFloat?) -> Self { + .anyTextModifier(TextWidthModifier(width)) + } + + @inline(__always) + static func foregroundStyle(_ style: S) -> Self where S: ShapeStyle { + .anyTextModifier(TextForegroundStyleModifier(style)) + } + + @inline(__always) + static func foregroundKeyColor() -> Self { + .anyTextModifier(TextForegroundKeyColorModifier()) + } +} + final class StrikethroughTextModifier: AnyTextModifier { let lineStyle: Text.LineStyle? diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift deleted file mode 100644 index 443f2b43b..000000000 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextUnimplemented.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// TextUnimplemented.swift -// OpenSwiftUI -// -// Created by Kyle on 6/7/26. -// - -extension Text { - func strikethrough(_ active: Bool = true, action: Color? = nil) -> Text { - self - } -} From ed7882902349b71f15c39a99a6c6d50d66411f7e Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 13 Jul 2026 01:50:33 +0800 Subject: [PATCH 12/24] Add Text renderer API documentation --- .../View/Text/Text/Text+Renderer.swift | 379 ++++++++++++++++-- 1 file changed, 337 insertions(+), 42 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift index b34b13440..a7bf48dbb 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift @@ -49,9 +49,9 @@ extension Text { /// Only one attribute of each type may be attached to each text /// view, with inner attributes taking precedence. /// - /// - Parameter value: The attribute to attach. + /// - Parameter value: the attribute to attach. /// - /// - Returns: A version of the text view with `value` attached. + /// - Returns: a version of the text view with `value` attached. public func customAttribute(_ value: T) -> Text where T: TextAttribute { modified(with: .customAttribute(value)) } @@ -209,51 +209,11 @@ extension Text { lhs.value < rhs.value } - /// Returns a value that is offset the specified distance from this value. - /// - /// Use the `advanced(by:)` method in generic code to offset a value by a - /// specified distance. If you're working directly with numeric values, use - /// the addition operator (`+`) instead of this method. - /// - /// func addOne(to x: T) -> T - /// where T.Stride: ExpressibleByIntegerLiteral - /// { - /// return x.advanced(by: 1) - /// } - /// - /// let x = addOne(to: 5) - /// // x == 6 - /// let y = addOne(to: 3.5) - /// // y = 4.5 - /// - /// If this type's `Stride` type conforms to `BinaryInteger`, then for a - /// value `x`, a distance `n`, and a value `y = x.advanced(by: n)`, - /// `x.distance(to: y) == n`. Using this method with types that have a - /// noninteger `Stride` may result in an approximation. If the result of - /// advancing by `n` is not representable as a value of this type, then a - /// runtime error may occur. - /// - /// - Parameter n: The distance to advance this value. - /// - Returns: A value that is offset from this value by `n`. - /// - /// - Complexity: O(1) @_alwaysEmitIntoClient public func advanced(by n: Int) -> Text.Layout.CharacterIndex { .init(value: value + n) } - /// Returns the distance from this value to the given value, expressed as a - /// stride. - /// - /// If this type's `Stride` type conforms to `BinaryInteger`, then for two - /// values `x` and `y`, and a distance `n = x.distance(to: y)`, - /// `x.advanced(by: n) == y`. Using this method with types that have a - /// noninteger `Stride` may result in an approximation. - /// - /// - Parameter other: The value to calculate the distance to. - /// - Returns: The distance from this value to `other`. - /// - /// - Complexity: O(1) @_alwaysEmitIntoClient public func distance(to other: Text.Layout.CharacterIndex) -> Int { other.value - value @@ -340,6 +300,7 @@ extension Text { _openSwiftUIUnimplementedFailure() } + /// The typographic bounds of the line. public var typographicBounds: Text.Layout.TypographicBounds { _openSwiftUIUnimplementedFailure() } @@ -378,6 +339,7 @@ extension Text { // MARK: - Text.Layout.Run [WIP] + /// A run of placed glyphs in a text layout. public struct Run: RandomAccessCollection, Equatable { @_spi(Private) @available(OpenSwiftUI_v6_0, *) @@ -408,18 +370,25 @@ extension Text { RunSlice(run: self, indices: bounds) } + /// The custom attribute of type `T` associated with the + /// run of glyphs, or nil. If no run contains the custom + /// attribute we also check its attachment's runs. public subscript(key: T.Type) -> T? where T: TextAttribute { _openSwiftUIUnimplementedFailure() } + /// The layout direction of the text run. public var layoutDirection: LayoutDirection { _openSwiftUIUnimplementedFailure() } + /// The typographic bounds of the run of glyphs. public var typographicBounds: Text.Layout.TypographicBounds { _openSwiftUIUnimplementedFailure() } + /// The array of character indices corresponding to the + /// glyphs in `self`. public var characterIndices: [Text.Layout.CharacterIndex] { _openSwiftUIUnimplementedFailure() } @@ -463,15 +432,20 @@ extension Text { _openSwiftUIUnimplementedFailure() } + /// The custom attribute of type `T` associated with the + /// run of glyphs, or nil. @_alwaysEmitIntoClient public subscript(key: T.Type) -> T? where T: TextAttribute { run[key] } + /// The typographic bounds of the partial run of glyphs. public var typographicBounds: Text.Layout.TypographicBounds { _openSwiftUIUnimplementedFailure() } + /// The array of character indices corresponding to the + /// glyphs in `self`. public var characterIndices: [Text.Layout.CharacterIndex] { _openSwiftUIUnimplementedFailure() } @@ -590,11 +564,50 @@ extension EnvironmentValues { @available(OpenSwiftUI_v1_0, *) extension Text { + /// Sets the color of the text displayed by this view. + /// + /// Use this method to change the color of the text rendered by a text view. + /// + /// For example, you can display the names of the colors red, green, and + /// blue in their respective colors: + /// + /// HStack { + /// Text("Red").foregroundColor(.red) + /// Text("Green").foregroundColor(.green) + /// Text("Blue").foregroundColor(.blue) + /// } + /// + /// ![Three text views arranged horizontally, each containing + /// the name of a color displayed in that + /// color.](OpenSwiftUI-Text-foregroundColor.png) + /// + /// - Parameter color: The color to use when displaying this text. + /// - Returns: A text view that uses the color value you supply. @available(*, deprecated, renamed: "foregroundStyle(_:)") public func foregroundColor(_ color: Color?) -> Text { modified(with: .color(color)) } + /// Sets the style of the text displayed by this view. + /// + /// Use this method to change the rendering style of the text + /// rendered by a text view. + /// + /// For example, you can display the names of the colors red, + /// green, and blue in their respective colors: + /// + /// HStack { + /// Text("Red").foregroundStyle(.red) + /// Text("Green").foregroundStyle(.green) + /// Text("Blue").foregroundStyle(.blue) + /// } + /// + /// ![Three text views arranged horizontally, each containing + /// the name of a color displayed in that + /// color.](OpenSwiftUI-Text-foregroundColor.png) + /// + /// - Parameter style: The style to use when displaying this text. + /// - Returns: A text view that uses the color value you supply. @available(OpenSwiftUI_v5_0, *) public func foregroundStyle(_ style: S) -> Text where S: ShapeStyle { if let color = style as? Color { @@ -604,52 +617,189 @@ extension Text { } } + /// Sets the default font for text in the view. + /// + /// Use `font(_:)` to apply a specific font to an individual + /// Text View, or all of the text views in a container. + /// + /// In the example below, the first text field has a font set directly, + /// while the font applied to the following container applies to all of the + /// text views inside that container: + /// + /// VStack { + /// Text("Font applied to a text view.") + /// .font(.largeTitle) + /// + /// VStack { + /// Text("These two text views have the same font") + /// Text("applied to their parent view.") + /// } + /// .font(.system(size: 16, weight: .light, design: .default)) + /// } + /// + /// + /// ![Applying a font to a single text view or a view container](OpenSwiftUI-view-font.png) + /// + /// - Parameter font: The font to use when displaying this text. + /// - Returns: Text that uses the font you specify. public func font(_ font: Font?) -> Text { modified(with: .font(font)) } + /// Sets the font weight of the text. + /// + /// - Parameter weight: One of the available font weights. + /// + /// - Returns: Text that uses the font weight you specify. public func fontWeight(_ weight: Font.Weight?) -> Text { modified(with: .weight(weight)) } + /// Sets the font width of the text. + /// + /// - Parameter width: One of the available font widths. + /// + /// - Returns: Text that uses the font width you specify, if available. @available(OpenSwiftUI_v4_0, *) public func fontWidth(_ width: Font.Width?) -> Text { modified(with: .width(width?.value)) } + /// Applies a bold or emphasized treatment to the fonts of the text. + /// + /// For fonts created from text styles, it could mean applying emphasized + /// styling, which does not necessarily mean the bold weight specifically, + /// so this modifier is not to be confused with ``Text/fontWeight(_:)``. + /// + /// For example: + /// + /// Text("hello").font(.body).bold() + /// + /// will most likely get you the emphasized version of body text style, + /// which is often in ``Font/Weight/semibold`` weight. While + /// + /// Text("hello").font(.body).fontWeight(.bold) + /// + /// will specifically get you the body text style font in the + /// ``Font/Weight/bold`` weight. + /// + /// - Returns: Bold or emphasized text. public func bold() -> Text { modified(with: .bold()) } + /// Applies a bold font weight to the text. + /// + /// - Parameter isActive: A Boolean value that indicates + /// whether text has bold styling. + /// + /// - Returns: Bold text. @available(OpenSwiftUI_v4_0, *) public func bold(_ isActive: Bool) -> Text { modified(with: .bold(isActive)) } + /// Applies italics to the text. + /// + /// - Returns: Italic text. public func italic() -> Text { modified(with: .italic()) } + /// Applies italics to the text. + /// + /// - Parameter isActive: A Boolean value that indicates + /// whether italic styling is added. + /// + /// - Returns: Italic text. @available(OpenSwiftUI_v4_0, *) public func italic(_ isActive: Bool) -> Text { modified(with: .italic(isActive)) } + /// Modifies the font of the text to use the fixed-width variant + /// of the current font, if possible. + /// + /// - Parameter isActive: A Boolean value that indicates + /// whether monospaced styling is added. Default value is `true`. + /// + /// - Returns: Monospaced text. @available(OpenSwiftUI_v4_4, *) public func monospaced(_ isActive: Bool = true) -> Text { modified(with: .monospaced(isActive)) } + /// Sets the font design of the text. + /// + /// - Parameter design: One of the available font designs. + /// + /// - Returns: Text that uses the font design you specify. @available(OpenSwiftUI_v4_1, *) public func fontDesign(_ design: Font.Design?) -> Text { modified(with: .design(design)) } + /// Modifies the text view's font to use fixed-width digits, while leaving + /// other characters proportionally spaced. + /// + /// This modifier only affects numeric characters, and leaves all other + /// characters unchanged. + /// + /// The following example shows the effect of `monospacedDigit()` on a + /// text view. It arranges two text views in a ``VStack``, each displaying + /// a formatted date that contains many instances of the character 1. + /// The second text view uses the `monospacedDigit()`. Because 1 is + /// usually a narrow character in proportional fonts, applying the + /// modifier widens all of the 1s, and the text view as a whole. + /// The non-digit characters in the text view remain unaffected. + /// + /// let myDate = DateComponents( + /// calendar: Calendar(identifier: .gregorian), + /// timeZone: TimeZone(identifier: "EST"), + /// year: 2011, + /// month: 1, + /// day: 11, + /// hour: 11, + /// minute: 11 + /// ).date! + /// + /// var body: some View { + /// VStack(alignment: .leading) { + /// Text(myDate.formatted(date: .long, time: .complete)) + /// .font(.system(size: 20)) + /// Text(myDate.formatted(date: .long, time: .complete)) + /// .font(.system(size: 20)) + /// .monospacedDigit() + /// } + /// .padding() + /// .navigationTitle("monospacedDigit() Modifier") + /// } + /// + /// ![Two vertically stacked text views, displaying the date January 11, + /// 2011, 11:11:00 AM. The second text view uses fixed-width digits, causing + /// all of the 1s to be wider than in the first text + /// view.](Text-monospacedDigit-1) + /// + /// If the base font of the text view doesn't support fixed-width digits, + /// the font remains unchanged. + /// + /// - Returns: A text view with a modified font that uses fixed-width + /// numeric characters, while leaving other characters proportionally + /// spaced. @available(OpenSwiftUI_v3_0, *) public func monospacedDigit() -> Text { modified(with: .monospacedDigit()) } + /// Applies a strikethrough to the text. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has a + /// strikethrough applied. + /// - color: The color of the strikethrough. If `color` is `nil`, the + /// strikethrough uses the default foreground color. + /// + /// - Returns: Text with a line through its center. public func strikethrough( _ isActive: Bool = true, color: Color? = nil @@ -659,6 +809,16 @@ extension Text { )) } + /// Applies a strikethrough to the text. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether strikethrough + /// is added. The default value is `true`. + /// - pattern: The pattern of the line. + /// - color: The color of the strikethrough. If `color` is `nil`, the + /// strikethrough uses the default foreground color. + /// + /// - Returns: Text with a line through its center. @available(OpenSwiftUI_v4_0, *) public func strikethrough( _ isActive: Bool = true, @@ -670,6 +830,15 @@ extension Text { )) } + /// Applies an underline to the text. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether the text has an + /// underline. + /// - color: The color of the underline. If `color` is `nil`, the + /// underline uses the default foreground color. + /// + /// - Returns: Text with a line running along its baseline. public func underline( _ isActive: Bool = true, color: Color? = nil @@ -679,6 +848,16 @@ extension Text { )) } + /// Applies an underline to the text. + /// + /// - Parameters: + /// - isActive: A Boolean value that indicates whether underline + /// styling is added. The default value is `true`. + /// - pattern: The pattern of the line. + /// - color: The color of the underline. If `color` is `nil`, the + /// underline uses the default foreground color. + /// + /// - Returns: Text with a line running along its baseline. @available(OpenSwiftUI_v4_0, *) public func underline( _ isActive: Bool = true, @@ -690,14 +869,130 @@ extension Text { )) } + /// Sets the spacing, or kerning, between characters. + /// + /// Kerning defines the offset, in points, that a text view should shift + /// characters from the default spacing. Use positive kerning to widen the + /// spacing between characters. Use negative kerning to tighten the spacing + /// between characters. + /// + /// VStack(alignment: .leading) { + /// Text("ABCDEF").kerning(-3) + /// Text("ABCDEF") + /// Text("ABCDEF").kerning(3) + /// } + /// + /// The last character in the first case, which uses negative kerning, + /// experiences cropping because the kerning affects the trailing edge of + /// the text view as well. + /// + /// ![Three text views showing character groups, with progressively + /// increasing spacing between the characters in each + /// group.](OpenSwiftUI-Text-kerning-1.png) + /// + /// Kerning attempts to maintain ligatures. For example, the Hoefler Text + /// font uses a ligature for the letter combination _ffl_, as in the word + /// _raffle_, shown here with a small negative and a small positive kerning: + /// + /// ![Two text views showing the word raffle in the Hoefler Text font, the + /// first with small negative and the second with small positive kerning. + /// The letter combination ffl has the same shape in both variants because + /// it acts as a ligature.](OpenSwiftUI-Text-kerning-2.png) + /// + /// The *ffl* letter combination keeps a constant shape as the other letters + /// move together or apart. Beyond a certain point in either direction, + /// however, kerning does disable nonessential ligatures. + /// + /// ![Two text views showing the word raffle in the Hoefler Text font, the + /// first with large negative and the second with large positive kerning. + /// The letter combination ffl does not act as a ligature in either + /// case.](OpenSwiftUI-Text-kerning-3.png) + /// + /// - Important: If you add both the ``Text/tracking(_:)`` and + /// ``Text/kerning(_:)`` modifiers to a view, the view applies the + /// tracking and ignores the kerning. + /// + /// - Parameter kerning: The spacing to use between individual characters in + /// this text. Value of `0` sets the kerning to the system default value. + /// + /// - Returns: Text with the specified amount of kerning. public func kerning(_ kerning: CGFloat) -> Text { modified(with: .kerning(kerning)) } + /// Sets the tracking for the text. + /// + /// Tracking adds space, measured in points, between the characters in the + /// text view. A positive value increases the spacing between characters, + /// while a negative value brings the characters closer together. + /// + /// VStack(alignment: .leading) { + /// Text("ABCDEF").tracking(-3) + /// Text("ABCDEF") + /// Text("ABCDEF").tracking(3) + /// } + /// + /// The code above uses an unusually large amount of tracking to make it + /// easy to see the effect. + /// + /// ![Three text views showing character groups with progressively + /// increasing spacing between the characters in each + /// group.](OpenSwiftUI-Text-tracking.png) + /// + /// The effect of tracking resembles that of the ``Text/kerning(_:)`` + /// modifier, but adds or removes trailing whitespace, rather than changing + /// character offsets. Also, using any nonzero amount of tracking disables + /// nonessential ligatures, whereas kerning attempts to maintain ligatures. + /// + /// - Important: If you add both the ``Text/tracking(_:)`` and + /// ``Text/kerning(_:)`` modifiers to a view, the view applies the + /// tracking and ignores the kerning. + /// + /// - Parameter tracking: The amount of additional space, in points, that + /// the view should add to each character cluster after layout. Value of `0` + /// sets the tracking to the system default value. + /// + /// - Returns: Text with the specified amount of tracking. public func tracking(_ tracking: CGFloat) -> Text { modified(with: .tracking(tracking)) } + /// Sets the vertical offset for the text relative to its baseline. + /// + /// Change the baseline offset to move the text in the view (in points) up + /// or down relative to its baseline. The bounds of the view expand to + /// contain the moved text. + /// + /// HStack(alignment: .top) { + /// Text("Hello") + /// .baselineOffset(-10) + /// .border(Color.red) + /// Text("Hello") + /// .border(Color.green) + /// Text("Hello") + /// .baselineOffset(10) + /// .border(Color.blue) + /// } + /// .background(Color(white: 0.9)) + /// + /// By drawing a border around each text view, you can see how the text + /// moves, and how that affects the view. + /// + /// ![Three text views, each with the word "Hello" outlined by a border and + /// aligned along the top edges. The first and last are larger than the + /// second, with padding inside the border above the word "Hello" in the + /// first case, and padding inside the border below the word in the last + /// case.](OpenSwiftUI-Text-baselineOffset.png) + /// + /// The first view, with a negative offset, grows downward to handle the + /// lowered text. The last view, with a positive offset, grows upward. The + /// enclosing ``HStack`` instance, shown in gray, ensures all the text views + /// remain aligned at their top edge, regardless of the offset. + /// + /// - Parameter baselineOffset: The amount to shift the text vertically (up + /// or down) relative to its baseline. + /// + /// - Returns: Text that's above or below its baseline. public func baselineOffset(_ baselineOffset: CGFloat) -> Text { modified(with: .baseline(baselineOffset)) } From 8402d0cca73fecb0c4c60e15f3c9221e5502789a Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Jul 2026 00:12:09 +0800 Subject: [PATCH 13/24] Fix NSTextAttachment header --- .../Shims/UIFoundation/NSTextAttachment.h | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h index 7948c2b87..f56be2d92 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h @@ -8,22 +8,86 @@ #if OPENSWIFTUI_TARGET_OS_DARWIN -// Modified based on macOS 27.0 SDK and iOS 18.5 SDK +// Modified based on macOS 26.2 SDK and iOS 18.5/26.2 SDKs. Keep the +// declarations aligned with AppKit and UIKit because clients can import both +// those modules and UIFoundation_Private. #import #import NS_HEADER_AUDIT_BEGIN(nullability, sendability) +@class NSTextContainer; +@class NSLayoutManager; +@class NSFileWrapper; +@class NSTextAttachmentViewProvider; +@class NSTextLayoutManager; +@protocol NSTextLocation; + +#if OPENSWIFTUI_TARGET_OS_OSX +@class NSImage; +@class NSView; +@class NSTextAttachmentCell; +@protocol NSTextAttachmentCell; +#else +@class UIImage; +@class UIView; +#endif + +API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)) +@protocol NSTextAttachmentLayout + +#if OPENSWIFTUI_TARGET_OS_OSX +- (nullable NSImage *)imageForBounds:(CGRect)bounds attributes:(NSDictionary *)attributes location:(id )location textContainer:(nullable NSTextContainer *)textContainer API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)); +#else +- (nullable UIImage *)imageForBounds:(CGRect)bounds attributes:(NSDictionary *)attributes location:(id )location textContainer:(nullable NSTextContainer *)textContainer API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)); +#endif + +- (CGRect)attachmentBoundsForAttributes:(NSDictionary *)attributes location:(id )location textContainer:(nullable NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedLineFragment position:(CGPoint)position API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)); + +#if OPENSWIFTUI_TARGET_OS_OSX +- (nullable NSTextAttachmentViewProvider *)viewProviderForParentView:(nullable NSView *)parentView location:(id )location textContainer:(nullable NSTextContainer *)textContainer API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), visionos(1.0)) API_UNAVAILABLE(watchos); +#else +- (nullable NSTextAttachmentViewProvider *)viewProviderForParentView:(nullable UIView *)parentView location:(id )location textContainer:(nullable NSTextContainer *)textContainer API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), visionos(1.0)) API_UNAVAILABLE(watchos); +#endif + +@end + OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)) -@interface NSTextAttachment : NSObject +@interface NSTextAttachment : NSObject + +- (instancetype)initWithData:(nullable NSData *)contentData ofType:(nullable NSString *)uti NS_DESIGNATED_INITIALIZER API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); + +#if OPENSWIFTUI_TARGET_OS_OSX +- (instancetype)initWithFileWrapper:(nullable NSFileWrapper *)fileWrapper; +#endif + +@property (nullable, copy, NS_NONATOMIC_IOSONLY) NSData *contents API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@property (nullable, copy, NS_NONATOMIC_IOSONLY) NSString *fileType API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); + +#if OPENSWIFTUI_TARGET_OS_OSX +@property (nullable, strong) NSImage *image API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +#else +@property (nullable, strong, NS_NONATOMIC_IOSONLY) UIImage *image API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +#endif + +@property (NS_NONATOMIC_IOSONLY) CGRect bounds API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@property (nullable, strong, NS_NONATOMIC_IOSONLY) NSFileWrapper *fileWrapper; + +#if OPENSWIFTUI_TARGET_OS_OSX +@property (nullable, strong) id attachmentCell API_AVAILABLE(macos(10.0)) API_UNAVAILABLE(ios, watchos, tvos, visionos); +#endif + +@property CGFloat lineLayoutPadding API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)); + ++ (nullable Class)textAttachmentViewProviderClassForFileType:(NSString *)fileType API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), visionos(1.0)) API_UNAVAILABLE(watchos); ++ (void)registerTextAttachmentViewProviderClass:(Class)textAttachmentViewProviderClass forFileType:(NSString *)fileType API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), visionos(1.0)) API_UNAVAILABLE(watchos); -@property (nullable, copy) NSData *contents; -@property (nullable, copy) NSString *fileType; -@property CGRect bounds; +@property BOOL allowsTextAttachmentView API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), visionos(1.0)) API_UNAVAILABLE(watchos); +@property (readonly) BOOL usesTextAttachmentView API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), visionos(1.0)) API_UNAVAILABLE(watchos); @end NS_HEADER_AUDIT_END(nullability, sendability) -#endif \ No newline at end of file +#endif From 1dbcb5476a9260881f53af79b4bbdc66d2b0323a Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Jul 2026 01:09:19 +0800 Subject: [PATCH 14/24] Update Paragraph API --- .../View/Text/Resolve/ResolvedText.swift | 28 +++++++++++ .../View/Text/Text/TextNonDarwinShims.swift | 50 +++++++++++++++++-- .../Shims/UIFoundation/NSParagraphStyle.h | 11 ++++ .../Shims/UIFoundation/NSParagraphStyle.m | 37 ++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.m diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index dfe2aacc4..e770f01a3 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -457,6 +457,34 @@ extension Text { } } +// MARK: - Text.ResolvedProperties.Paragraph + Extension + +extension Text.ResolvedProperties.Paragraph { + package mutating func style(environment: EnvironmentValues) -> NSParagraphStyle { + if let cachedStyle { + return cachedStyle + } + let style = makeParagraphStyle(environment: environment) + style.compositionLanguage = compositionLanguage + if environment.shouldRedactContent { + style.fullyJustified = true + style.baseWritingDirection = switch environment.layoutDirection { + case .leftToRight: .leftToRight + case .rightToLeft: .rightToLeft + } + style.lineBreakMode = .byCharWrapping + } + cachedStyle = style + return style + } + + package mutating func markParagraphBoundary(_ paragraphBoundary: Bool) { + if paragraphBoundary { + cachedStyle = nil + } + } +} + // MARK: - EnvironmentValues + disableLinkColor extension EnvironmentValues { diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift index 5af584035..1a08bb9ca 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift @@ -8,9 +8,53 @@ public import UIFoundation_Private public typealias NSInteger = Int -package class NSParagraphStyle: NSObject {} -package class NSMutableParagraphStyle: NSObject {} -package class TextAttachment: NSObject {} +package enum NSWritingDirection: Int { + case natural = -1 + case leftToRight = 0 + case rightToLeft = 1 +} + +package enum NSLineBreakMode: Int { + case byWordWrapping = 0 + case byCharWrapping + case byClipping + case byTruncatingHead + case byTruncatingTail + case byTruncatingMiddle +} + +package class NSParagraphStyle: NSObject { + fileprivate var _compositionLanguage: NSCompositionLanguage = .unset + fileprivate var _fullyJustified = false + fileprivate var _baseWritingDirection: NSWritingDirection = .natural + fileprivate var _lineBreakMode: NSLineBreakMode = .byWordWrapping +} + +extension NSParagraphStyle { + package var compositionLanguage: NSCompositionLanguage { + get { _compositionLanguage } + set { _compositionLanguage = newValue } + } + + package var fullyJustified: Bool { + get { _fullyJustified } + set { _fullyJustified = newValue } + } + + package var baseWritingDirection: NSWritingDirection { + get { _baseWritingDirection } + set { _baseWritingDirection = newValue } + } + + package var lineBreakMode: NSLineBreakMode { + get { _lineBreakMode } + set { _lineBreakMode = newValue } + } +} + +package class NSMutableParagraphStyle: NSParagraphStyle {} + +package class NSTextAttachment: NSObject {} extension NSMutableAttributedString { package var isEmptyOrTerminatedByParagraphSeparator: Bool { diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h index b7c060b95..3187d2977 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h @@ -20,6 +20,7 @@ #import #import "NSText.h" +#import "NSCompositionLanguage.h" @class NSTextList; @class NSTextBlock; @@ -184,6 +185,16 @@ OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), @end +@interface NSMutableParagraphStyle (OpenSwiftUI_SPI) + +@property (nonatomic) NSCompositionLanguage compositionLanguage_openswiftui_safe_wrapper + OPENSWIFTUI_SWIFT_NAME(compositionLanguage); + +@property (nonatomic) BOOL fullyJustified_openswiftui_safe_wrapper + OPENSWIFTUI_SWIFT_NAME(fullyJustified); + +@end + NS_HEADER_AUDIT_END(nullability, sendability) #endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.m b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.m new file mode 100644 index 000000000..00a92396d --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.m @@ -0,0 +1,37 @@ +// +// NSParagraphStyle.m +// OpenSwiftUI_SPI + + +#include "NSParagraphStyle.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import "../OpenSwiftUIShims.h" +#import + +@implementation NSMutableParagraphStyle (OpenSwiftUI_SPI) + +- (NSCompositionLanguage)compositionLanguage_openswiftui_safe_wrapper { + OPENSWIFTUI_SAFE_WRAPPER_IMP(NSCompositionLanguage, @"compositionLanguage", NSCompositionLanguageUnset); + return func(self, selector); +} + +- (void)setCompositionLanguage_openswiftui_safe_wrapper:(NSCompositionLanguage)compositionLanguage { + OPENSWIFTUI_SAFE_WRAPPER_IMP(void, @"setCompositionLanguage:", , NSCompositionLanguage); + func(self, selector, compositionLanguage); +} + +- (BOOL)fullyJustified_openswiftui_safe_wrapper { + OPENSWIFTUI_SAFE_WRAPPER_IMP(BOOL, @"fullyJustified", NO); + return func(self, selector); +} + +- (void)setFullyJustified_openswiftui_safe_wrapper:(BOOL)fullyJustified { + OPENSWIFTUI_SAFE_WRAPPER_IMP(void, @"setFullyJustified:", , BOOL); + func(self, selector, fullyJustified); +} + +@end + +#endif From 3a8d5c4c1bbdd81481214f62b648afb14e4cb35e Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 14 Jul 2026 02:27:50 +0800 Subject: [PATCH 15/24] Update Text.ResolvedProperties --- .../View/Text/Resolve/ResolvedText.swift | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index e770f01a3..b4989752c 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -362,8 +362,9 @@ extension Text { _openSwiftUIUnimplementedFailure() } } - - // FIXME + + // MARK: - Text.ResolvedProperties + package struct ResolvedProperties { package var insets: EdgeInsets = .zero @@ -374,7 +375,9 @@ extension Text { package var transitions: [Text.ResolvedProperties.Transition] = [] package var suffix: ResolvedTextSuffix = .none - + + // MARK: - Text.ResolvedProperties.CustomAttachments + package struct CustomAttachments { package var characterIndices: [Int] @@ -390,13 +393,15 @@ extension Text { package var customAttachments: Text.ResolvedProperties.CustomAttachments = .init() package init() { - _openSwiftUIUnimplementedWarning() + _openSwiftUIEmptyStub() } package mutating func registerCustomAttachment(at offset: Int) { - _openSwiftUIUnimplementedFailure() + customAttachments.characterIndices.append(offset) } - + + // MARK: - Text.ResolvedProperties.Features + package struct Features: OptionSet { package let rawValue: UInt16 @@ -415,14 +420,16 @@ extension Text { package static let useTextLayoutManager: Text.ResolvedProperties.Features = .init(rawValue: 1 << 4) package static let useTextSuffix: Text.ResolvedProperties.Features = .init(rawValue: 1 << 5) - + package static let produceTextLayout: Text.ResolvedProperties.Features = .init(rawValue: 1 << 6) - - package static let checkInterpolationStrategy: Text.ResolvedProperties.Features = .init(rawValue: 1 << 8) - + + package static let checkInterpolationStrategy: Text.ResolvedProperties.Features = .init(rawValue: 1 << 7) + package static let isUniqueSizeVariant: Text.ResolvedProperties.Features = .init(rawValue: 1 << 8) } - + + // MARK: - Text.ResolvedProperties.Transition + package struct Transition: Equatable { package var transition: ContentTransition @@ -430,29 +437,54 @@ extension Text { self.transition = transition } } - + + // MARK: - Text.ResolvedProperties.Paragraph + package struct Paragraph { - package var compositionLanguage: NSCompositionLanguage - + package var compositionLanguage: NSCompositionLanguage = .unset + var cachedStyle: NSParagraphStyle? } - package var paragraph: Text.ResolvedProperties.Paragraph = .init(compositionLanguage: .none) - + package var paragraph: Text.ResolvedProperties.Paragraph = .init() + package mutating func addColor(_ c: Color.Resolved) { - _openSwiftUIUnimplementedFailure() + if c.linearRed == -1, c.linearGreen == -1 { + features.insert(.keyColor) + } } package mutating func addAttachment() { - _openSwiftUIUnimplementedFailure() + features.insert(.attachments) } package mutating func addSensitive() { features.insert(.sensitive) } - package mutating func addCustomStyle(_ style: _ShapeStyle_Pack.Style) -> Color.Resolved { - _openSwiftUIUnimplementedFailure() + package mutating func addCustomStyle( + _ style: ShapeStyle.Pack.Style + ) -> Color.Resolved { + if let color = style.color { + return color.multiplyingOpacity(by: style.opacity) + } + var index = 0 + while index < styles.count { + if styles[index] == style { + break + } + index += 1 + } + if index == styles.count { + styles.append(style) + features.insert(.keyColor) + } + return Color.Resolved( + linearRed: -1, + linearGreen: -1, + linearBlue: Float(index) / 1024, + opacity: 1 + ) } } } From 647f85dfebd1cdf05e4c93a4b061d2b4f82658fc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 18 Jul 2026 17:38:04 +0800 Subject: [PATCH 16/24] Add OpenSwiftUITextAttachment --- .../Graphic/GraphicsContext.swift | 26 ++++++++++++++ .../View/Text/Resolve/ResolvedText.swift | 35 +++++++++++++++++++ .../View/Text/Text/TextNonDarwinShims.swift | 6 +++- .../Graphic/GraphicsContextTestsStub.c | 1 + 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift index 6bda9b3a8..f1522c76c 100644 --- a/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift +++ b/Sources/OpenSwiftUICore/Graphic/GraphicsContext.swift @@ -512,6 +512,9 @@ public struct GraphicsContext { public static var inverse: ClipOptions { Self(rawValue: 1 << 0) } } + // FIXME + public struct Shading: Sendable {} + // FIXME package enum ResolvedShading: Sendable { case backdrop(Color.Resolved) @@ -523,6 +526,29 @@ public struct GraphicsContext { } extension GraphicsContext { + #if OPENSWIFTUI_SWIFTUI_RENDERER + @_silgen_name("OpenSwiftUITestStub_GraphicsContextDrawImageInRectStyleShading") + private func swiftUI_draw( + _ image: GraphicsImage, + in rect: CGRect, + style: FillStyle, + shading: Shading? + ) + #endif + + package func draw( + _ image: GraphicsImage, + in rect: CGRect, + style: FillStyle, + shading: Shading? + ) { + #if OPENSWIFTUI_SWIFTUI_RENDERER + swiftUI_draw(image, in: rect, style: style, shading: shading) + #else + _openSwiftUIUnimplementedWarning() + #endif + } + #if OPENSWIFTUI_SWIFTUI_RENDERER @_silgen_name("OpenSwiftUITestStub_GraphicsContextDrawPathWithShadingAndStyle") private func swiftUI_draw(_ path: Path, with shading: GraphicsContext.ResolvedShading, style: PathDrawingStyle) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index b4989752c..9152aead0 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -517,6 +517,41 @@ extension Text.ResolvedProperties.Paragraph { } } +// MARK: - OpenSwiftUITextAttachment + +class OpenSwiftUITextAttachment: NSTextAttachment { + let graphicsImage: GraphicsImage + let alignmentOrigin: CGPoint + + init(image: Image.Resolved) { + graphicsImage = image.image + if case .vectorGlyph = graphicsImage.contents { + alignmentOrigin = image.alignmentOrigin + } else { + alignmentOrigin = .zero + } + super.init(data: nil, ofType: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + _openSwiftUIUnreachableCode() + } + + func draw(in context: inout GraphicsContext, at point: CGPoint) { + let origin = CGPoint( + x: alignmentOrigin.x + point.x, + y: alignmentOrigin.y + point.y + ) + context.draw( + graphicsImage, + in: CGRect(origin: origin, size: graphicsImage.size), + style: FillStyle(), + shading: nil + ) + } +} + // MARK: - EnvironmentValues + disableLinkColor extension EnvironmentValues { diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift index 1a08bb9ca..73a85c943 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift @@ -54,7 +54,11 @@ extension NSParagraphStyle { package class NSMutableParagraphStyle: NSParagraphStyle {} -package class NSTextAttachment: NSObject {} +package class NSTextAttachment: NSObject { + package init(data: Data?, ofType: String?) { + super.init() + } +} extension NSMutableAttributedString { package var isEmptyOrTerminatedByParagraphSeparator: Bool { diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Graphic/GraphicsContextTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Graphic/GraphicsContextTestsStub.c index 0c1eb5e6e..0bb09aac2 100644 --- a/Sources/OpenSwiftUISymbolDualTestsSupport/Graphic/GraphicsContextTestsStub.c +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Graphic/GraphicsContextTestsStub.c @@ -9,5 +9,6 @@ #import DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_GraphicsContextDrawPathWithShadingAndStyle, SwiftUI, $s7SwiftUI15GraphicsContextVAAE4draw_4with5styleyAA4PathV_AC15ResolvedShadingOAA0H12DrawingStyleOtF); +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_GraphicsContextDrawImageInRectStyleShading, SwiftUI, $s7SwiftUI15GraphicsContextVAAE4draw_2in5style7shadingyAA0C5ImageV_So6CGRectVAA9FillStyleVAC7ShadingVSgtF); #endif From 1736ada91e947089694916c0594a4d5b9ff3514d Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 18 Jul 2026 18:42:12 +0800 Subject: [PATCH 17/24] Update Text.Style.TextStyleFont --- .../View/Text/Font/ModifiedFont.swift | 2 +- .../View/Text/Resolve/ResolvedText.swift | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Font/ModifiedFont.swift b/Sources/OpenSwiftUICore/View/Text/Font/ModifiedFont.swift index d01e2b173..4230a762d 100644 --- a/Sources/OpenSwiftUICore/View/Text/Font/ModifiedFont.swift +++ b/Sources/OpenSwiftUICore/View/Text/Font/ModifiedFont.swift @@ -600,7 +600,7 @@ extension AnyFontModifier { guard let weight = self as? AnyDynamicFontModifier else { return false } - return weight.modifier.weight.value >= Font.Weight.bold.value + return weight.modifier.weight.value >= Font.Weight.semibold.value } } diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 9152aead0..76bf5fe85 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -343,23 +343,43 @@ extension Text { } package func fontTraits(in environment: EnvironmentValues) -> Font.ResolvedTraits { - _openSwiftUIUnimplementedFailure() + let font = baseFont.resolve(in: environment, includeDefaultAttributes: true) + let environmentModifiers = environment.fontModifiers + if fontModifiers.isEmpty && environmentModifiers.isEmpty { + return font!.resolveTraits(in: environment.fontResolutionContext) + } + let context = environment.fontResolutionContext + var descriptor = font!.resolve(in: context) + for modifier in environmentModifiers { + modifier.modify(descriptor: &descriptor, in: context) + } + for modifier in fontModifiers { + modifier.modify(descriptor: &descriptor, in: context) + } + return .init(descriptor) } package mutating func addFontModifier(_ modifier: M) where M: FontModifier { - _openSwiftUIUnimplementedFailure() + fontModifiers.append(.dynamic(modifier)) } package mutating func addFontModifier(type: M.Type) where M: StaticFontModifier { - _openSwiftUIUnimplementedFailure() + fontModifiers.append(.static(M.self)) } package mutating func removeFontModifier(ofType _: M.Type) where M: FontModifier { - _openSwiftUIUnimplementedFailure() + let typeID = ObjectIdentifier(M.self) + fontModifiers.removeAll { $0 is AnyDynamicFontModifier } + clearedFontModifiers.insert(typeID) } package mutating func removeFontModifier(ofType _: M.Type) where M: StaticFontModifier { - _openSwiftUIUnimplementedFailure() + let typeID = ObjectIdentifier(M.self) + fontModifiers.removeAll { + $0 is AnyStaticFontModifier || + (M.self == Font.BoldModifier.self && $0.isboldFontWeightModifier) + } + clearedFontModifiers.insert(typeID) } } From 324e0f1cb2de4f4316f220d860c2fbc374c320ae Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 18 Jul 2026 19:42:35 +0800 Subject: [PATCH 18/24] Update Text.Style.TextStyleColor --- .../OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 76bf5fe85..f2135e1a9 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -259,7 +259,7 @@ extension Text { } } - // MARK: - Text.Style.TextStyleColor [WIP] + // MARK: - Text.Style.TextStyleColor package enum TextStyleColor { case implicit @@ -282,7 +282,7 @@ extension Text { guard !options.contains(.foregroundKeyColor) else { return .init(linearWhite: -1, opacity: 1) } - let s = environment.defaultForegroundStyle + let s = environment.foregroundStyle style = .init(s?.fallbackColor(in: environment, level: 0) ?? .primary) case .explicit(let anyShapeStyle): style = anyShapeStyle @@ -293,7 +293,7 @@ extension Text { guard !options.contains(.foregroundKeyColor) else { return .init(linearWhite: -1, opacity: 1) } - let s = environment.foregroundStyle + let s = environment.defaultForegroundStyle style = .init(s?.fallbackColor(in: environment, level: 0) ?? .primary) case .foregroundKeyColor(let base): guard !options.contains(.allowsKeyColors) else { From da89ead6b99d1904ca71c1070bf7dab383b672bb Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 18 Jul 2026 21:02:22 +0800 Subject: [PATCH 19/24] Update Text.Style --- .../View/Text/Resolve/ResolvedText.swift | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index f2135e1a9..dda7c2544 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -195,38 +195,37 @@ extension Text { } } - // MARK: - Text.Style [WIP] + // MARK: - Text.Style package struct Style { - internal var baseFont: TextStyleFont = .default - internal var fontModifiers: [AnyFontModifier] = [] - internal var color: TextStyleColor = .default - internal var backgroundColor: Color? - internal var baselineOffset: CGFloat? - internal var kerning: CGFloat? - internal var tracking: CGFloat? - internal var strikethrough: LineStyle = .default - internal var underline: LineStyle = .default - internal var encapsulation: Text.Encapsulation? - internal var speech: AccessibilitySpeechAttributes? + var baseFont: TextStyleFont = .implicit + var fontModifiers: [AnyFontModifier] = [] + var color: TextStyleColor = .implicit + var backgroundColor: Color? + var baselineOffset: CGFloat? + var kerning: CGFloat? + var tracking: CGFloat? + var strikethrough: LineStyle = .implicit + var underline: LineStyle = .implicit + var encapsulation: Text.Encapsulation? + var speech: AccessibilitySpeechAttributes? package var accessibility: AccessibilityTextAttributes? -#if canImport(CoreText) - internal var glyphInfo: CTGlyphInfo? -#endif - // internal var shadow: TextShadowModifier? - // internal var transition: TextTransitionModifier? - internal var scale: Text.Scale? - internal var superscript: Text.Superscript? - internal var typesettingConfiguration: TypesettingConfiguration = .init() - internal var customAttributes: [TextAttributeModifierBase] = [] -#if canImport(Darwin) - internal var adaptiveImageGlyph: AttributedString.AdaptiveImageGlyph? -#endif + #if canImport(CoreText) + var glyphInfo: CTGlyphInfo? + #endif + var shadow: TextShadowModifier? + var transition: TextTransitionModifier? + var scale: Text.Scale? + var superscript: Text.Superscript? + var typesettingConfiguration: TypesettingConfiguration = .init() + var customAttributes: [TextAttributeModifierBase] = [] + #if canImport(Darwin) + var adaptiveImageGlyph: AttributedString.AdaptiveImageGlyph? + #endif package var clearedFontModifiers: Set = [] init() { - // FIXME - _openSwiftUIUnimplementedWarning() + _openSwiftUIEmptyStub() } // MARK: - Text.Style.LineStyle From 9311d95f2bc3a65de18248b61e451e145b6a89b3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 18 Jul 2026 23:30:05 +0800 Subject: [PATCH 20/24] Add ReducedTimelineSchedule --- .../Text+AttributedString.swift | 87 +++++++++++ .../Text+NSAttributedString.swift | 10 +- .../Text/Util/ReducedTimelineSchedule.swift | 28 ++++ .../View/Text/Util/String+Extension.swift | 139 ++++++------------ 4 files changed, 163 insertions(+), 101 deletions(-) create mode 100644 Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift diff --git a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift index d39c647bd..f6d69e338 100644 --- a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift +++ b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift @@ -532,6 +532,93 @@ extension AttributedString { } } +extension AttributedString { + package var isStyled: Bool { + runs.contains { run in + #if canImport(CoreText) + let hasAdaptiveImageGlyph = run.adaptiveImageGlyph != nil + #endif + if run.font != nil { + return true + } + if run.foregroundColor != nil { + return true + } + if run.backgroundColor != nil { + return true + } + if run.strikethroughStyle != nil { + return true + } + if run.underlineStyle != nil { + return true + } + if run.kern != nil { + return true + } + if run.tracking != nil { + return true + } + if run.baselineOffset != nil { + return true + } + if run.textScale != nil { + return true + } + if run.superscript != nil { + return true + } + if run.privateStrikethroughColor != nil { + return true + } + if run.privateUnderlineColor != nil { + return true + } + if let inlinePresentationIntent = run.inlinePresentationIntent, + !inlinePresentationIntent.intersection([ + .emphasized, + .stronglyEmphasized, + .strikethrough, + .code, + ]).isEmpty { + return true + } + if run.link != nil { + return true + } + #if canImport(CoreText) + if hasAdaptiveImageGlyph { + return true + } + #endif + return false + } + } +} + +extension NSAttributedString { + convenience init(openSwiftUIAttributedString attributedString: AttributedString) { + #if canImport(Darwin) + let transformedAttributedString = CoreGlue2.shared.transformingEquivalentAttributes(attributedString) + do { + let nsAttributedString = try NSAttributedString( + transformedAttributedString, + including: \.openSwiftUI + ) + self.init(attributedString: nsAttributedString) + } catch { + Log.runtimeIssues( + "AttributedString %@ has invalid attributes. A plain string will be used instead.", + [attributedString.description] + ) + self.init(string: String(attributedString.characters)) + } + #else + self.init(string: String(attributedString.characters)) + #endif + } +} + extension NSMutableAttributedString { func convertToPlatformStyled( style: Text.Style, diff --git a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift index 7c377f44b..44a9d8f50 100644 --- a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift +++ b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift @@ -22,17 +22,15 @@ let kCTTextScaleRatioAttributeName: CFString #endif extension NSAttributedString.Key { - package static let resolvableAttributeConfiguration: NSAttributedString.Key = .init("OpenSwiftUI.resolvableAttributeConfiguration") + static let resolvableAttributeConfiguration: NSAttributedString.Key = .init("OpenSwiftUI.resolvableAttributeConfiguration") - package static let _textScale: NSAttributedString.Key = .init("NSTextScale") - - package static let kitTracking: NSAttributedString.Key = .init("CTTracking") + static let _textScale: NSAttributedString.Key = .init("NSTextScale") #if canImport(CoreText) - package static let _textScaleRatio: NSAttributedString.Key = .init(kCTTextScaleRatioAttributeName as String) + static let _textScaleRatio: NSAttributedString.Key = .init(kCTTextScaleRatioAttributeName as String) #endif - package static let _textScaleStaticWeightMatching: NSAttributedString.Key = .init("NSTextScaleStaticWeightMatching") + static let _textScaleStaticWeightMatching: NSAttributedString.Key = .init("NSTextScaleStaticWeightMatching") } extension NSAttributedString { diff --git a/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift b/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift new file mode 100644 index 000000000..76e1e9d5a --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift @@ -0,0 +1,28 @@ +// +// ReducedTimelineSchedule.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP + +package import Foundation + +// TODO: ReducedTimelineSchedule + +// MARK: - NSAttributedString + Extension + +extension NSAttributedString { + package var isDynamic: Bool { + guard length >= 1 else { return false } + let value = attribute( + .updateSchedule, + at: 0, + effectiveRange: nil + ) + return value != nil + } + + var updateSchedule: any TimelineSchedule { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Util/String+Extension.swift b/Sources/OpenSwiftUICore/View/Text/Util/String+Extension.swift index 8b913e6fb..98ae2869f 100644 --- a/Sources/OpenSwiftUICore/View/Text/Util/String+Extension.swift +++ b/Sources/OpenSwiftUICore/View/Text/Util/String+Extension.swift @@ -3,10 +3,12 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete package import Foundation +// MARK: - String + Extension + extension String { package static var nsAttachment: String { "\u{FFFC}" @@ -17,113 +19,60 @@ extension String { } } +// MARK: - Character + Extension + extension Character { package static var nsAttachment: Character { Character(.nsAttachment) } } +// MARK: - AttributedString + Extension + extension AttributedString { package var isEmpty: Bool { characters.isEmpty } +} - package var isStyled: Bool { - runs.contains { run in - #if canImport(CoreText) - let hasAdaptiveImageGlyph = run.adaptiveImageGlyph != nil - #endif - if run.font != nil { - return true - } - if run.foregroundColor != nil { - return true - } - if run.backgroundColor != nil { - return true - } - if run.strikethroughStyle != nil { - return true - } - if run.underlineStyle != nil { - return true - } - if run.kern != nil { - return true - } - if run.tracking != nil { - return true - } - if run.baselineOffset != nil { - return true - } - if run.textScale != nil { - return true - } - if run.superscript != nil { - return true - } - if run.privateStrikethroughColor != nil { - return true - } - if run.privateUnderlineColor != nil { - return true - } - if let inlinePresentationIntent = run.inlinePresentationIntent, - !inlinePresentationIntent.intersection([ - .emphasized, - .stronglyEmphasized, - .strikethrough, - .code, - ]).isEmpty { - return true - } - if run.link != nil { - return true - } - #if canImport(CoreText) - if hasAdaptiveImageGlyph { - return true - } - #endif - return false - } - } +// MARK: - NSAttributedString.Key + Kit Extension + +extension NSAttributedString.Key { + static let kitFont: NSAttributedString.Key = .init("NSFont") + + static let kitParagraphStyle: NSAttributedString.Key = .init("NSParagraphStyle") + + static let kitForegroundColor: NSAttributedString.Key = .init("NSColor") + + static let kitBackgroundColor: NSAttributedString.Key = .init("NSBackgroundColor") + + static let kitKern: NSAttributedString.Key = .init("NSKern") + + static let kitTracking: NSAttributedString.Key = .init("CTTracking") + + static let kitStrikethroughStyle: NSAttributedString.Key = .init("NSStrikethrough") + + static let kitUnderlineStyle: NSAttributedString.Key = .init("NSUnderline") + + static let kitShadow: NSAttributedString.Key = .init("NSShadow") + + static let kitAttachment: NSAttributedString.Key = .init("NSAttachment") + + static let kitLink: NSAttributedString.Key = .init("NSLink") + + static let kitBaselineOffset: NSAttributedString.Key = .init("NSBaselineOffset") + + static let kitUnderlineColor: NSAttributedString.Key = .init("NSUnderlineColor") + + static let kitStrikethroughColor: NSAttributedString.Key = .init("NSStrikethroughColor") } -extension NSAttributedString { - convenience package init(openSwiftUIAttributedString attributedString: AttributedString) { - #if canImport(Darwin) - let transformedAttributedString = CoreGlue2.shared.transformingEquivalentAttributes(attributedString) - do { - let nsAttributedString = try NSAttributedString( - transformedAttributedString, - including: \.openSwiftUI - ) - self.init(attributedString: nsAttributedString) - } catch { - Log.runtimeIssues( - "AttributedString %@ has invalid attributes. A plain string will be used instead.", - [attributedString.description] - ) - self.init(string: String(attributedString.characters)) - } - #else - self.init(string: String(attributedString.characters)) - #endif - } +#if canImport(CoreText) +import CoreText - package var isDynamic: Bool { - guard length >= 1 else { return false } - let value = attribute( - .updateSchedule, - at: 0, - effectiveRange: nil - ) - return value != nil - } - - var updateSchedule: any TimelineSchedule { - _openSwiftUIUnimplementedFailure() +extension NSAttributedString { + func kitFont(at index: Int) -> CTFont? { + attribute(.kitFont, at: index, effectiveRange: nil) as! CTFont? } } +#endif From 741a5d7eace668d52bab4f6e9185244123ed931f Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 19 Jul 2026 00:13:22 +0800 Subject: [PATCH 21/24] Update Text.Resolved --- .../Text+AttributedString.swift | 3 +- .../View/Text/Resolve/ResolvedText.swift | 110 +++++++++++++++--- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift index f6d69e338..27dedec8c 100644 --- a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift +++ b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+AttributedString.swift @@ -654,9 +654,8 @@ extension NSMutableAttributedString { } } -@available(OpenSwiftUI_v6_0, *) extension Dictionary where Key == NSAttributedString.Key, Value == Any { - fileprivate mutating func transferAttributedStringStyles(to style: inout Text.Style) { + mutating func transferAttributedStringStyles(to style: inout Text.Style) { let fontKey = NSAttributedString.Key(AttributeScopes.OpenSwiftUIAttributes.FontAttribute.name) if let font = self[fontKey] as? Font { style.baseFont = .explicit(font) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index dda7c2544..c949c889e 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 7AFAB46D18FA6D189589CFA78D8B2B2E (SwiftUICore) package import Foundation @@ -82,15 +82,14 @@ extension ResolvedTextContainer { } extension Text { + + // MARK: - Text.Resolved + package struct Resolved: ResolvedTextContainer { package var style: Text.Style = .init() - package var attributedString: NSMutableAttributedString? - package var includeDefaultAttributes: Bool = true - package var idiom: AnyInterfaceIdiom? - package var properties: Text.ResolvedProperties = .init() package init() { @@ -103,7 +102,7 @@ extension Text { with options: Text.ResolveOptions, isUniqueSizeVariant: Bool ) where S: StringProtocol { - var string = String(string).caseConvertedIfNeeded(env) + let string = String(string).caseConvertedIfNeeded(env) let attributes = style.nsAttributes( content: { string }, environment: env, @@ -115,7 +114,9 @@ extension Text { if attributedString!.isEmptyOrTerminatedByParagraphSeparator { properties.paragraph.cachedStyle = nil } - _openSwiftUIUnimplementedWarning() + if isUniqueSizeVariant { + properties.features.formUnion(.isUniqueSizeVariant) + } } package mutating func append( @@ -124,13 +125,47 @@ extension Text { with options: Text.ResolveOptions, isUniqueSizeVariant: Bool ) { - // FIXME: Workaround append issue + attributedString.enumerateAttributes( + in: NSRange(location: 0, length: attributedString.length) + ) { sourceAttributes, range, _ in + var attributes = sourceAttributes + var style = self.style + attributes.transferAttributedStringStyles(to: &style) + if sourceAttributes[.kitLink] != nil { + let foregroundColorKey = NSAttributedString.Key(AttributeScopes.OpenSwiftUIAttributes.ForegroundColorAttribute.name) + if !options.contains(.disableLinkColor), + sourceAttributes[foregroundColorKey] == nil { + let linkColor = env.tintColor + ?? env.resolvedTextProvider?.defaultLinkColor(for: env) + ?? .accentColor + style.color = .explicit(AnyShapeStyle(linkColor)) + } + } + let string = attributedString + .attributedSubstring(from: range) + .string + .caseConvertedIfNeeded(env) + let platformAttributes = style.nsAttributes( + content: { string }, + environment: env, + includeDefaultAttributes: includeDefaultAttributes, + with: options, + properties: &properties + ) + attributes.merge(platformAttributes) { old, _ in old } + append(string, with: attributes, in: env) + if self.attributedString!.isEmptyOrTerminatedByParagraphSeparator { + properties.paragraph.cachedStyle = nil + } + } if self.attributedString == nil { - self.attributedString = NSMutableAttributedString(attributedString: attributedString) - } else { - self.attributedString?.append(attributedString) + self.attributedString = NSMutableAttributedString( + attributedString: attributedString + ) + } + if isUniqueSizeVariant { + properties.features.formUnion(.isUniqueSizeVariant) } - _openSwiftUIUnimplementedWarning() } package mutating func append( @@ -138,7 +173,45 @@ extension Text { in environment: EnvironmentValues, with options: Text.ResolveOptions ) { - _openSwiftUIUnimplementedFailure() + var attributes = nsAttributes( + content: nil, + in: environment, + with: options, + properties: &properties + ) + if !environment.shouldRedactContent { + let resolvedImage: Image.Resolved + if let foregroundColor = style.color.resolve( + in: environment, + with: options, + properties: &properties + ) { + resolvedImage = image.foregroundColor { foregroundColor } + } else { + resolvedImage = image + } + + let attachment = OpenSwiftUITextAttachment(image: resolvedImage) + if options.contains(.includeAccessibility), + !resolvedImage.decorative, + let label = resolvedImage.label { +// attachment.accessibilityLabel = label.text.resolveString( +// in: environment, +// with: options +// ) + } + environment.resolvedTextProvider?.updateImageTextAttachment( + in: attachment, + image: resolvedImage + ) + attributes[.kitAttachment] = attachment + } + append( + .nsAttachment, + with: attributes, + in: environment + ) + properties.features.formUnion(.attachments) } package mutating func append( @@ -164,7 +237,13 @@ extension Text { with options: Text.ResolveOptions, properties: inout Text.ResolvedProperties ) -> [NSAttributedString.Key: Any] { - _openSwiftUIUnimplementedFailure() + style.nsAttributes( + content: content, + environment: environment, + includeDefaultAttributes: includeDefaultAttributes, + with: options, + properties: &properties + ) } private mutating func append( @@ -642,8 +721,7 @@ extension Text { append( attributedString.string, in: env, - with: options, - isUniqueSizeVariant: isUniqueSizeVariant + with: options ) } From ca12ee5c8b1873bc2d91c73145d2acfef7277af3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 19 Jul 2026 00:41:34 +0800 Subject: [PATCH 22/24] Update append for Image.Resolved --- .../View/Text/Resolve/ResolvedText.swift | 29 +++++++++---------- .../OpenSwiftUICore/View/Text/Text/Text.swift | 2 +- .../View/Text/Text/TextNonDarwinShims.swift | 6 ++++ .../Shims/UIFoundation/NSTextAttachment.h | 6 ++++ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index c949c889e..1df6bac3a 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -180,25 +180,24 @@ extension Text { properties: &properties ) if !environment.shouldRedactContent { - let resolvedImage: Image.Resolved - if let foregroundColor = style.color.resolve( - in: environment, - with: options, - properties: &properties - ) { - resolvedImage = image.foregroundColor { foregroundColor } - } else { - resolvedImage = image + let resolvedImage = image.foregroundColor { + let color = style.color.resolve( + in: environment, + with: options, + properties: &properties, + includeDefaultAttributes: true + )! + properties.addColor(color) + return color } - let attachment = OpenSwiftUITextAttachment(image: resolvedImage) if options.contains(.includeAccessibility), !resolvedImage.decorative, let label = resolvedImage.label { -// attachment.accessibilityLabel = label.text.resolveString( -// in: environment, -// with: options -// ) + attachment.accessibilityLabel = label.text.resolveString( + in: environment, + with: options + ) } environment.resolvedTextProvider?.updateImageTextAttachment( in: attachment, @@ -211,7 +210,7 @@ extension Text { with: attributes, in: environment ) - properties.features.formUnion(.attachments) + properties.addAttachment() } package mutating func append( diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift index 013419cd1..4b69bb33e 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text.swift @@ -418,7 +418,7 @@ public struct Text: Equatable, Sendable { switch storage { case let .verbatim(string): return string - case let .anyTextStorage: + case .anyTextStorage: var resolved = Text.ResolvedString() resolved.idiom = idiom resolve(into: &resolved, in: environment, with: options) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift index 73a85c943..2eafb11fc 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift @@ -55,9 +55,15 @@ extension NSParagraphStyle { package class NSMutableParagraphStyle: NSParagraphStyle {} package class NSTextAttachment: NSObject { + override init() { + super.init() + } + package init(data: Data?, ofType: String?) { super.init() } + + package var accessibilityLabel: String? } extension NSMutableAttributedString { diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h index f56be2d92..8a2afa619 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSTextAttachment.h @@ -88,6 +88,12 @@ OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), @end +@interface NSTextAttachment (OpenSwiftUI_SPI) + +@property (nullable, strong) NSString *accessibilityLabel; + +@end + NS_HEADER_AUDIT_END(nullability, sendability) #endif From 37f4c98b13fee1c3c2c20e0d58813eed3905e6cc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 19 Jul 2026 01:29:22 +0800 Subject: [PATCH 23/24] Update append for Image.NamedResolved --- .../View/Text/Resolve/ResolvedText.swift | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 1df6bac3a..02885603c 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -218,7 +218,29 @@ extension Text { in environment: EnvironmentValues, with options: Text.ResolveOptions ) { - _openSwiftUIUnimplementedFailure() + guard options.contains(.writeAuxiliaryMetadata) else { + return + } + var attributes = nsAttributes( + content: nil, + in: environment, + with: options, + properties: &properties + ) + if !environment.shouldRedactContent { + let attachment = NSTextAttachment() + environment.resolvedTextProvider?.updateWidgetTextAttachment( + attachment, + namedImage: namedImage + ) + attributes[.kitAttachment] = attachment + } + append( + .nsAttachment, + with: attributes, + in: environment + ) + properties.addAttachment() } package mutating func append( From acdade96d054267afe35bd9312f81e27b4d844f9 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 19 Jul 2026 01:53:17 +0800 Subject: [PATCH 24/24] Update append for ResolvableStringAttribute --- .../Resolve/ResolvableStringAttribute.swift | 18 ---- .../ResolvableTextSegmentAttribute.swift | 95 +++++++++++++++++++ .../View/Text/Resolve/ResolvedText.swift | 37 +++++++- 3 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTextSegmentAttribute.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift index de19c5407..1d79ace64 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableStringAttribute.swift @@ -168,21 +168,3 @@ extension EnvironmentValues { 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/Resolve/ResolvableTextSegmentAttribute.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTextSegmentAttribute.swift new file mode 100644 index 000000000..0ac6db811 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvableTextSegmentAttribute.swift @@ -0,0 +1,95 @@ +// +// ResolvableTextSegmentAttribute.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete-Stubbed +// ID: E9C99F480CB4DD26488FF949B5D8B9E1 (SwiftUICore) + +package import Foundation + +// MARK: - NSAttributedString.Key + resolvableTextSegment + +extension NSAttributedString.Key { + package static let resolvableTextSegment: NSAttributedString.Key = .init(ResolvableTextSegmentAttribute.name) +} + +// MARK: - ResolvableTextSegmentAttribute [TODO] + +package enum ResolvableTextSegmentAttribute: CodableAttributedStringKey { + // FIXME + package struct Value: Codable, Hashable { + package func isAttributeRequiredForResolution( + _ attribute: NSAttributedString.Key, + includeNonFunctionalAttributes: Bool + ) -> Bool { + _openSwiftUIUnimplementedFailure() + } + } + + package static let name: String = "OpenSwiftUI.resolvableTextSegment" +} + +extension ResolvableTextSegmentAttribute { + package static func legacySegment( + resolvableAttributeKey: NSAttributedString.Key, + length: Int + ) -> Value { + _openSwiftUIUnimplementedFailure() + } + + package static func toggleAttributes(in string: NSMutableAttributedString) { + _openSwiftUIUnimplementedFailure() + } + + package static func update( + _ string: NSMutableAttributedString, + in context: ResolvableStringResolutionContext + ) { + _openSwiftUIUnimplementedFailure() + } +} + +extension ResolvableTextSegmentAttribute { + package static func buildDynamicTextSegment( + for resolvable: R, + style: Text.Style, + environment: EnvironmentValues, + includeDefaultAttributes: Bool, + options: Text.ResolveOptions, + properties: inout Text.ResolvedProperties + ) -> NSMutableAttributedString? where R: ResolvableStringAttribute { + _openSwiftUIUnimplementedWarning() + return nil + } + + package static func buildStaticTextSegment( + for resolvable: R, + style: Text.Style, + environment: EnvironmentValues, + includeDefaultAttributes: Bool, + options: Text.ResolveOptions, + properties: inout Text.ResolvedProperties + ) -> NSMutableAttributedString? where R: ResolvableStringAttribute { + _openSwiftUIUnimplementedWarning() + return nil + } +} + +// MARK: - PlatformAttributeResolver [TODO] + +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/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 02885603c..fec553c3f 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -249,7 +249,42 @@ extension Text { with options: Text.ResolveOptions, transition: ContentTransition? ) where R: ResolvableStringAttribute { - _openSwiftUIUnimplementedFailure() + var style = style + if style.transition == nil, var transition { + transition.isReplaceable = true + style.transition = TextTransitionModifier(transition) + } + let attributedString: NSMutableAttributedString? + if environment.redactionReasons.contains(.placeholder) { + attributedString = ResolvableTextSegmentAttribute.buildStaticTextSegment( + for: resolvable, + style: style, + environment: environment, + includeDefaultAttributes: includeDefaultAttributes, + options: options, + properties: &properties + ) + } else { + attributedString = ResolvableTextSegmentAttribute.buildDynamicTextSegment( + for: resolvable, + style: style, + environment: environment, + includeDefaultAttributes: includeDefaultAttributes, + options: options, + properties: &properties + ) + } + guard let attributedString else { + Log.internalWarning("Unable to resolve custom attribute \(resolvable)") + return + } + if let current = self.attributedString { + current.append(attributedString) + } else { + self.attributedString = NSMutableAttributedString( + attributedString: attributedString + ) + } } package func nsAttributes(