diff --git a/.github/actions/run-uitests/action.yml b/.github/actions/run-uitests/action.yml deleted file mode 100644 index 9d639041c..000000000 --- a/.github/actions/run-uitests/action.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Run UI Tests -description: Execute UI tests and collect artifacts - -inputs: - platform: - description: 'Platform to test (ios or macos)' - required: true - destination: - description: 'xcodebuild destination parameter' - required: true - artifact-name: - description: 'Name for the artifact upload' - required: true - -outputs: - test-result: - description: 'Test execution result (success or failure)' - value: ${{ steps.uitest.outcome }} - -runs: - using: composite - steps: - - name: Record baseline images with SwiftUI - shell: bash - run: | - cd Example - xcodebuild test \ - -scheme OpenSwiftUIUITests \ - -configuration SwiftUIDebug \ - -destination "${{ inputs.destination }}" \ - -skipMacroValidation \ - -skipPackagePluginValidation || true - - - name: Run UI tests with OpenSwiftUI - id: uitest - continue-on-error: true - shell: bash - run: | - cd Example - xcodebuild test \ - -scheme OpenSwiftUIUITests \ - -configuration OpenSwiftUIDebug \ - -destination "${{ inputs.destination }}" \ - -skipMacroValidation \ - -skipPackagePluginValidation 2>&1 | tee /tmp/${{ inputs.platform }}-uitest.log - - - name: Collect failed snapshots - if: always() - shell: bash - run: | - Scripts/CI/collect_uitest_failures.sh /tmp/${{ inputs.platform }}-uitest.log /tmp/uitest-artifacts - - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: ${{ inputs.artifact-name }} - path: /tmp/uitest-artifacts/ - retention-days: 7 diff --git a/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift b/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift index 204a1b049..c34fbed6e 100644 --- a/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift +++ b/Sources/OpenSwiftUI/Animation/Timeline/TimelineView.swift @@ -225,7 +225,7 @@ extension TimelineView: View, PrimitiveView, UnaryView where Content: View { schedule: view.value[offset: { .of(&$0.schedule) }], phase: inputs.viewPhase, time: inputs.time, - referenceDate: inputs.base.referenceDate, + referenceDate: inputs.referenceDate, id: id, frameSpecifier: inputs.base.alwaysOnFrameSpecifier, fidelity: inputs.base.updateFidelity, diff --git a/Sources/OpenSwiftUICore/Data/Environment/CachedEnvironment.swift b/Sources/OpenSwiftUICore/Data/Environment/CachedEnvironment.swift index 488101b1b..36b1e1dca 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/CachedEnvironment.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/CachedEnvironment.swift @@ -63,7 +63,8 @@ package struct CachedEnvironment { role: ShapeRole, mode: Attribute<_ShapeStyle_ResolverMode>? ) -> Attribute<_ShapeStyle_Pack> { - _openSwiftUIUnimplementedFailure() + _openSwiftUIUnimplementedWarning() + return ViewGraph.current.intern(.defaultValue, id: .defaultValue) } } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index fba3aa090..3a5eb79ef 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -529,7 +529,6 @@ extension GraphicsImage: ProtobufMessage { } package struct ResolvedShadowStyle {} -package struct StyledTextContentView: PrimitiveView {} package struct RasterizationOptions {} package protocol RBDisplayListContents {} // RenderBox.RBDisplayListContents public struct PlatformDrawableOptions {} diff --git a/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift b/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift index 2c93df7bf..1678efedb 100644 --- a/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift +++ b/Sources/OpenSwiftUICore/Render/GeometryEffect/GeometryEffect.swift @@ -136,24 +136,23 @@ extension GeometryEffectProvider { newInputs.containerPosition = zeroPoint newInputs.size = size var outputs = body(_Graph(), newInputs) - guard inputs.preferences.requiresDisplayList else { - return outputs - } - let identity = DisplayList.Identity() - inputs.pushIdentity(identity) - let displayList = Attribute( - GeometryEffectDisplayList( - identity: .init(), - effect: animatableEffect, - position: inputs.animatedPosition(), - size: inputs.animatedCGSize(), // Verify: Still get a new size here - layoutDirection: inputs.layoutDirection, - containerPosition: inputs.containerPosition, - content: .init(outputs.preferences.displayList), - options: .init() + if inputs.preferences.requiresDisplayList { + let identity = DisplayList.Identity() + inputs.pushIdentity(identity) + let displayList = Attribute( + GeometryEffectDisplayList( + identity: .init(), + effect: animatableEffect, + position: inputs.animatedPosition(), + size: inputs.animatedCGSize(), // Verify: Still get a new size here + layoutDirection: inputs.layoutDirection, + containerPosition: inputs.containerPosition, + content: .init(outputs.preferences.displayList), + options: .init() + ) ) - ) - outputs.preferences.displayList = displayList + outputs.displayList = displayList + } return outputs } } diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift index 939543930..9027315a6 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRenderedShape.swift @@ -8,6 +8,8 @@ extension ShapeStyle { package typealias RenderedShape = _ShapeStyle_RenderedShape + package typealias RenderedLayers = _ShapeStyle_RenderedLayers + package typealias LayerID = _ShapeStyle_LayerID package typealias InterpolatorGroup = _ShapeStyle_InterpolatorGroup } @@ -15,11 +17,51 @@ package struct _ShapeStyle_RenderedShape { package enum Shape { case empty case path(Path, FillStyle) - // case text(StyledTextContentView) - // case image(GraphicsImage) + case text(StyledTextContentView) + case image(GraphicsImage) case alphaMask(DisplayList.Item) } } +package struct _ShapeStyle_RenderedLayers { +} + +package enum _ShapeStyle_LayerID: Equatable { + case unstyled + case styled(_ShapeStyle_Name, UInt16) + case customStyle(Swift.UInt32) + case named(String?) +} + final package class _ShapeStyle_InterpolatorGroup/*: DisplayList.InterpolatorGroup*/ { + struct Layer { + let id: ShapeStyle.LayerID + + let serial: UInt32 + + var style: ShapeStyle.Pack.Style + + var state: DisplayList.InterpolatorLayer + + var isRemoved:Bool + } + + var layers: [Layer] = [] + + var contentsScale: Float = .zero + + // FIXME + var rasterizationOptions: RasterizationOptions = .init() + + var serial: UInt32 = .zero + + var cursor: Int32 = .zero + + init() { + _openSwiftUIEmptyStub() + } +} + +extension DisplayList { + struct InterpolatorLayer {} } diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift deleted file mode 100644 index 33eab916e..000000000 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeadView.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ShapeStyledLeadView.swift -// OpenSwiftUICore -// -// Audited for 6.0.87 -// Status: WIP - -package import Foundation -package import OpenAttributeGraphShims - -package protocol ShapeStyledLeafView: ContentResponder { - static var animatesSize: Bool { get } - - associatedtype ShapeUpdateData = Void - - mutating func mustUpdate(data: ShapeUpdateData, position: Attribute) -> Bool - - typealias FramedShape = (shape: ShapeStyle.RenderedShape.Shape, frame: CGRect) - - func shape(in size: CGSize) -> FramedShape - - static var hasBackground: Bool { get } - - func backgroundShape(in size: CGSize) -> FramedShape - - func isClear(styles: _ShapeStyle_Pack) -> Bool -} - -extension ShapeStyledLeafView { - package static var animatesSize: Bool { true } - - package static var hasBackground: Bool { false } - - package func backgroundShape(in size: CGSize) -> FramedShape { - (shape: .path(Path(), FillStyle()), frame: .zero) - } - - package func isClear(styles: ShapeStyle.Pack) -> Bool { - styles.isClear(name: .foreground) && styles.isClear(name: .background) - } - - package func contains(points: [PlatformPoint], size: CGSize) -> BitVector64 { - _openSwiftUIUnimplementedFailure() - } - - package func contentPath(size: CGSize) -> Path { - _openSwiftUIUnimplementedFailure() - } - - package static func makeLeafView( - view: _GraphValue, - inputs: _ViewInputs, - styles: Attribute, - interpolatorGroup: ShapeStyle.InterpolatorGroup? = nil, - data: ShapeUpdateData - ) -> _ViewOutputs { - _openSwiftUIUnimplementedFailure() - } -} - -extension ShapeStyledLeafView where ShapeUpdateData == () { - package mutating func mustUpdate(data: ShapeUpdateData, position: Attribute) -> Bool { - _openSwiftUIUnimplementedFailure() - } - - @inlinable - package static func makeLeafView( - view: _GraphValue, - inputs: _ViewInputs, - styles: Attribute, - interpolatorGroup: ShapeStyle.InterpolatorGroup? = nil, - data: ShapeUpdateData - ) -> _ViewOutputs { - _openSwiftUIUnimplementedFailure() - } -} - -package struct ShapeStyledResponderData: ContentResponder where V: ShapeStyledLeafView { - package func contains(points: [PlatformPoint], size: CGSize) -> BitVector64 { - _openSwiftUIUnimplementedFailure() - } - - package func contentPath(size: CGSize) -> Path { - _openSwiftUIUnimplementedFailure() - } -} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeafView.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeafView.swift new file mode 100644 index 000000000..bf10f96c2 --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyledLeafView.swift @@ -0,0 +1,189 @@ +// +// ShapeStyledLeafView.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: E1641985C375D8826E6966D4F238A1B8 + +package import Foundation +package import OpenAttributeGraphShims + +package protocol ShapeStyledLeafView: ContentResponder { + static var animatesSize: Bool { get } + + associatedtype ShapeUpdateData = Void + + mutating func mustUpdate(data: ShapeUpdateData, position: Attribute) -> Bool + + typealias FramedShape = (shape: ShapeStyle.RenderedShape.Shape, frame: CGRect) + + func shape(in size: CGSize) -> FramedShape + + static var hasBackground: Bool { get } + + func backgroundShape(in size: CGSize) -> FramedShape + + func isClear(styles: _ShapeStyle_Pack) -> Bool +} + +extension ShapeStyledLeafView { + package static var animatesSize: Bool { true } + + package static var hasBackground: Bool { false } + + package func backgroundShape(in size: CGSize) -> FramedShape { + (shape: .path(Path(), FillStyle()), frame: .zero) + } + + package func isClear(styles: ShapeStyle.Pack) -> Bool { + styles.isClear(name: .foreground) && styles.isClear(name: .background) + } + + package func contains(points: [PlatformPoint], size: CGSize) -> BitVector64 { + _openSwiftUIUnimplementedFailure() + } + + package func contentPath(size: CGSize) -> Path { + _openSwiftUIUnimplementedFailure() + } + + package static func makeLeafView( + view: _GraphValue, + inputs: _ViewInputs, + styles: Attribute, + interpolatorGroup: ShapeStyle.InterpolatorGroup? = nil, + data: ShapeUpdateData + ) -> _ViewOutputs { + var outputs = _ViewOutputs() + if inputs.preferences.requiresDisplayList { + + let identity = DisplayList.Identity() + inputs.pushIdentity(identity) + let displayList = Attribute( + ShapeStyledDisplayList( + group: interpolatorGroup, + identity: identity, + view: view.value, + styles: styles, + size: inputs.size.cgSize, + animatedSize: inputs.animatedSize(), + position: inputs.animatedPosition(), + containerPosition: inputs.containerPosition, + transform: inputs.transform, + environment: inputs.environment, + safeAreaInsets: inputs.safeAreaInsets, + options: inputs.displayListOptions, + data: data, + contentSeed: .init() + ) + ) + outputs.displayList = displayList + } + // TODO: Responder + return outputs + } +} + +extension ShapeStyledLeafView where ShapeUpdateData == () { + package mutating func mustUpdate( + data: ShapeUpdateData, + position: Attribute + ) -> Bool { + false + } + + @inlinable + package static func makeLeafView( + view: _GraphValue, + inputs: _ViewInputs, + styles: Attribute, + interpolatorGroup: ShapeStyle.InterpolatorGroup? = nil + ) -> _ViewOutputs { + makeLeafView( + view: view, + inputs: inputs, + styles: styles, + interpolatorGroup: interpolatorGroup, + data: () + ) + } +} + +package struct ShapeStyledResponderData: ContentResponder where V: ShapeStyledLeafView { + package func contains(points: [PlatformPoint], size: CGSize) -> BitVector64 { + _openSwiftUIUnimplementedFailure() + } + + package func contentPath(size: CGSize) -> Path { + _openSwiftUIUnimplementedFailure() + } +} + +// TODO: ShapeStyledResponderFilter + +// MARK: - ShapeStyledDisplayList [WIP] + +struct ShapeStyledDisplayList: StatefulRule, AsyncAttribute where V: ShapeStyledLeafView { + let group: ShapeStyle.InterpolatorGroup? + let identity: DisplayList.Identity + @Attribute var view: V + @Attribute var styles: ShapeStyle.Pack + @Attribute var size: CGSize + @Attribute var animatedSize: ViewSize + @Attribute var position: CGPoint + @Attribute var containerPosition: CGPoint + @Attribute var transform: ViewTransform + @Attribute var environment: EnvironmentValues + @OptionalAttribute var safeAreaInsets: SafeAreaInsets? + let options: DisplayList.Options + let data: V.ShapeUpdateData + var contentSeed: DisplayList.Seed + + init( + group: ShapeStyle.InterpolatorGroup?, + identity: DisplayList.Identity, + view: Attribute, + styles: Attribute, + size: Attribute, + animatedSize: Attribute, + position: Attribute, + containerPosition: Attribute, + transform: Attribute, + environment: Attribute, + safeAreaInsets: OptionalAttribute, + options: DisplayList.Options, + data: V.ShapeUpdateData, + contentSeed: DisplayList.Seed + ) { + self.group = group + self.identity = identity + self._view = view + self._styles = styles + self._size = size + self._animatedSize = animatedSize + self._position = position + self._containerPosition = containerPosition + self._transform = transform + self._environment = environment + self._safeAreaInsets = safeAreaInsets + self.options = options + self.data = data + self.contentSeed = contentSeed + } + + typealias Value = DisplayList + + func updateValue() { + // FIXME + let view = view as! StyledTextContentView + var item = DisplayList.Item( + .content(.init(.text(view, CGSize(width: 50, height: 50)), seed: .init())), + frame: .zero, + identity: identity, + version: .init() + ) + item.canonicalize(options: options) + value = DisplayList(item) + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift index 5438dcfb9..21db9054c 100644 --- a/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift +++ b/Sources/OpenSwiftUICore/View/Text/Resolve/ResolvedText.swift @@ -7,6 +7,9 @@ // ID: 7AFAB46D18FA6D189589CFA78D8B2B2E (SwiftUICore) package import Foundation +package import UIFoundation_Private + +// MARK: - ResolvedTextContainer package protocol ResolvedTextContainer { var style: Text.Style { get set } @@ -27,24 +30,24 @@ package protocol ResolvedTextContainer { isUniqueSizeVariant: Bool ) -// mutating func append( -// _ image: Image.Resolved, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions -// ) -// -// mutating func append( -// _ namedImage: Image.NamedResolved, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions -// ) -// -// mutating func append( -// resolvable: R, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions, -// transition: ContentTransition? -// ) where R: ResolvableStringAttribute + mutating func append( + _ image: Image.Resolved, + in environment: EnvironmentValues, + with options: Text.ResolveOptions + ) + + mutating func append( + _ namedImage: Image.NamedResolved, + in environment: EnvironmentValues, + with options: Text.ResolveOptions + ) + + mutating func append( + resolvable: R, + in environment: EnvironmentValues, + with options: Text.ResolveOptions, + transition: ContentTransition? + ) where R: ResolvableStringAttribute } extension ResolvedTextContainer { @@ -53,7 +56,12 @@ extension ResolvedTextContainer { in env: EnvironmentValues, with options: Text.ResolveOptions, ) where S: StringProtocol { - _openSwiftUIUnimplementedFailure() + append( + string, + in: env, + with: options, + isUniqueSizeVariant: env.textSizeVariant != .regular + ) } mutating func append( @@ -61,7 +69,12 @@ extension ResolvedTextContainer { in env: EnvironmentValues, with options: Text.ResolveOptions, ) { - _openSwiftUIUnimplementedFailure() + append( + attributedString, + in: env, + with: options, + isUniqueSizeVariant: env.textSizeVariant != .regular + ) } } @@ -87,6 +100,18 @@ extension Text { with options: Text.ResolveOptions, isUniqueSizeVariant: Bool ) where S: StringProtocol { + var string = String(string).caseConvertedIfNeeded(env) + let attributes = style.nsAttributes( + content: { string }, + environment: env, + includeDefaultAttributes: includeDefaultAttributes, + with: options, + properties: &properties + ) + append(string, with: attributes, in: env) + if attributedString!.isEmptyOrTerminatedByParagraphSeparator { + properties.paragraph.cachedStyle = nil + } _openSwiftUIUnimplementedFailure() } @@ -99,39 +124,66 @@ extension Text { _openSwiftUIUnimplementedFailure() } -// package mutating func append( -// _ image: Image.Resolved, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions -// ) { -// _openSwiftUIUnimplementedFailure() -// } -// -// package mutating func append( -// _ namedImage: Image.NamedResolved, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions -// ) { -// _openSwiftUIUnimplementedFailure() -// } -// -// package mutating func append( -// resolvable: R, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions, -// transition: ContentTransition? -// ) where R: ResolvableStringAttribute { -// _openSwiftUIUnimplementedFailure() -// } -// -// package func nsAttributes( -// content: (() -> String)?, -// in environment: EnvironmentValues, -// with options: Text.ResolveOptions, -// properties: inout Text.ResolvedProperties -// ) -> [NSAttributedString.Key: Any] { -// _openSwiftUIUnimplementedFailure() -// } + package mutating func append( + _ image: Image.Resolved, + in environment: EnvironmentValues, + with options: Text.ResolveOptions + ) { + _openSwiftUIUnimplementedFailure() + } + + package mutating func append( + _ namedImage: Image.NamedResolved, + in environment: EnvironmentValues, + with options: Text.ResolveOptions + ) { + _openSwiftUIUnimplementedFailure() + } + + package mutating func append( + resolvable: R, + in environment: EnvironmentValues, + with options: Text.ResolveOptions, + transition: ContentTransition? + ) where R: ResolvableStringAttribute { + _openSwiftUIUnimplementedFailure() + } + + package func nsAttributes( + content: (() -> String)?, + in environment: EnvironmentValues, + with options: Text.ResolveOptions, + properties: inout Text.ResolvedProperties + ) -> [NSAttributedString.Key: Any] { + _openSwiftUIUnimplementedFailure() + } + + private mutating func append( + _ string: String, + with attributes: [NSAttributedString.Key: Any], + in environment: EnvironmentValues + ) { + var string = string.caseConvertedIfNeeded(environment) + if environment.shouldRedactContent { + string = String.init(repeating: "􀮷", count: string.count) + } + if environment.sensitiveContent { + properties.addSensitive() + } + if let attributedString { + attributedString.append( + NSAttributedString( + string: string, + attributes: attributes + ) + ) + } else { + attributedString = NSMutableAttributedString( + string: string, + attributes: attributes + ) + } + } } // MARK: - Text.Style [WIP] @@ -336,7 +388,7 @@ extension Text { package let rawValue: UInt16 package init(rawValue: UInt16) { - _openSwiftUIUnimplementedFailure() + self.rawValue = rawValue } package static let keyColor: Text.ResolvedProperties.Features = .init(rawValue: 1 << 0) @@ -367,7 +419,9 @@ extension Text { } package struct Paragraph { - // package var compositionLanguage: NSCompositionLanguage + package var compositionLanguage: NSCompositionLanguage + + var cachedStyle: NSParagraphStyle? } package var paragraph: Text.ResolvedProperties.Paragraph @@ -381,7 +435,7 @@ extension Text { } package mutating func addSensitive() { - _openSwiftUIUnimplementedFailure() + features.insert(.sensitive) } package mutating func addCustomStyle(_ style: _ShapeStyle_Pack.Style) -> Color.Resolved { @@ -397,7 +451,9 @@ extension Text { var string: String = "" var hasResolvableAttributes: Bool = false - init() {} + init() { + _openSwiftUIEmptyStub() + } mutating func append( _ string: S, @@ -425,6 +481,44 @@ extension Text { isUniqueSizeVariant: isUniqueSizeVariant ) } + + mutating func append( + _ image: Image.Resolved, + in environment: EnvironmentValues, + with options: Text.ResolveOptions + ) { + string.append("") // object replacement character (U+FFFC) + } + + mutating func append( + _ namedImage: Image.NamedResolved, + in environment: EnvironmentValues, + with options: Text.ResolveOptions + ) { + string.append("") // object replacement character (U+FFFC) + } + + mutating func append( + resolvable: R, + in environment: EnvironmentValues, + with options: Text.ResolveOptions, + transition: ContentTransition? + ) where R: ResolvableStringAttribute { + let context = ResolvableStringResolutionContext( + referenceDate: nil, + environment: environment, + maximumWidth: nil + ) + guard let attributedString = resolvable.resolve(in: context) else { + Log.internalWarning("Unable to resolve custom attribute \(resolvable)") + return + } + append( + String(attributedString.characters), + in: environment, + with: options + ) + } } } diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift index 78d2ac56f..614131075 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Date.swift @@ -176,6 +176,14 @@ package struct ReferenceDateInput: ViewInput { } } +extension _ViewInputs { + @inline(__always) + package var referenceDate: WeakAttribute { + get { self[ReferenceDateInput.self] } + set { self[ReferenceDateInput.self] = newValue } + } +} + extension _GraphInputs { @inline(__always) package var referenceDate: WeakAttribute { diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+NSAttributedString.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+NSAttributedString.swift new file mode 100644 index 000000000..a561cd231 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+NSAttributedString.swift @@ -0,0 +1,117 @@ +// +// Text+NSAttributedString.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP + +package import Foundation +package import UIFoundation_Private + +package func makeParagraphStyle(environment: EnvironmentValues) -> NSMutableParagraphStyle { + let paragraphStyle = NSMutableParagraphStyle() + #if canImport(Darwin) + // TODO + #endif + return paragraphStyle +} + +#if canImport(CoreText) +@_silgen_name("kCTUIFontTextStyleTitle0") +let kCTTextScaleRatioAttributeName: CFString +#endif + +extension NSAttributedString.Key { + package static let resolvableAttributeConfiguration: NSAttributedString.Key = .init("OpenSwiftUI.resolvableAttributeConfiguration") + + package static let _textScale: NSAttributedString.Key = .init("NSTextScale") + + #if canImport(CoreText) + package static let _textScaleRatio: NSAttributedString.Key = .init(kCTTextScaleRatioAttributeName as String) + #endif + + package static let _textScaleStaticWeightMatching: NSAttributedString.Key = .init("NSTextScaleStaticWeightMatching") +} + +extension NSAttributedString { + package func firstAttribute(_ type: T.Type) -> T? where T: ResolvableStringAttribute { + _openSwiftUIUnimplementedFailure() + } +} + +extension NSMutableAttributedString { + package func addResolvableAttributes(with config: ResolvableAttributeConfiguration) { + _openSwiftUIUnimplementedFailure() + } + + package func resolveAttributes(in context: ResolvableStringResolutionContext) { + _openSwiftUIUnimplementedFailure() + } +} + +extension Text { + package func resolveAttributedString( + in environment: EnvironmentValues, + includeDefaultAttributes: Bool = true, + options: Text.ResolveOptions = [.includeSupportForRepeatedResolution], + idiom: AnyInterfaceIdiom? = nil + ) -> NSAttributedString? { + _openSwiftUIUnimplementedFailure() + } + + package func resolveAttributedStringAndProperties( + in environment: EnvironmentValues, + includeDefaultAttributes: Bool = true, + options: Text.ResolveOptions = [.includeSupportForRepeatedResolution], + idiom: AnyInterfaceIdiom? = nil + ) -> (NSAttributedString?, Text.ResolvedProperties) { + _openSwiftUIUnimplementedFailure() + } +} + +extension EnvironmentValues { + package func resolveNSAttributes( + includeDefaultAttributes: Bool = true, + options: Text.ResolveOptions = [] + ) -> [NSAttributedString.Key: Any] { + _openSwiftUIUnimplementedFailure() + } +} + +extension NSAttributedString { + package func scaled(by factor: CGFloat) -> NSAttributedString { + guard factor != 1.0 else { + return self + } + #if canImport(Darwin) + return _ui_attributedSubstring( + from: NSRange(location: 0, length: length), + scaledBy: factor + ) + #else + _openSwiftUIPlatformUnimplementedWarning() + return self + #endif + } + + package struct EncodedFontMetrics { + package var capHeight: CGFloat, ascender: CGFloat, descender: CGFloat, leading: CGFloat + package var outsets: EdgeInsets + } + + package var maxFontMetrics: EncodedFontMetrics { + _openSwiftUIUnimplementedFailure() + } +} + +extension Text.Style { + package func nsAttributes( + content: (() -> String)?, + environment: EnvironmentValues, + includeDefaultAttributes: Bool, + with options: Text.ResolveOptions, + properties: inout Text.ResolvedProperties + ) -> [NSAttributedString.Key: Any] { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift index 2ff8ca657..60db75e2b 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Renderer.swift @@ -5,3 +5,452 @@ // Audited for 6.5.4 // Status: Empty // ID: 7F70C8A76EE0356881289646072938C0 (SwiftUICore) + +import OpenAttributeGraphShims +public import OpenCoreGraphicsShims + +// TODO + +/// A proxy for a text view that custom text renderers use. +@available(OpenSwiftUI_v6_0, *) +public struct TextProxy { + + var text: ResolvedStyledText + + /// Returns the space needed by the text view, for a proposed size. + /// + /// - Parameter proposal: the proposed size of the text view. + /// + /// - Returns: the size that the text view requires for the + /// given proposal. + public func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize { + _openSwiftUIUnimplementedFailure() + } +} + +/// A value that you can attach to text views and that text renderers can query. +@available(OpenSwiftUI_v5_0, *) +public protocol TextAttribute {} + +// MARK: - TextRendererBoxBase + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public class TextRendererBoxBase { + let environment: EnvironmentValues + + init(environment: EnvironmentValues) { + self.environment = environment + } + + func draw(layout: Text.Layout, in context: inout GraphicsContext) { + _openSwiftUIBaseClassAbstractMethod() + } + + func sizeThatFits(proposal: ProposedViewSize, text: TextProxy) -> CGSize { + _openSwiftUIBaseClassAbstractMethod() + } + + var displayPadding: EdgeInsets { + _openSwiftUIBaseClassAbstractMethod() + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension TextRendererBoxBase: Sendable {} + +@available(OpenSwiftUI_v5_0, *) +extension Text { + // MARK: - Text.Layout + + /// A value describing the layout and custom attributes of a tree + /// of `Text` views. + public struct Layout: RandomAccessCollection, Equatable { + private var lines: [Text.Layout.Line] + + /// Indicates if this text is truncated. + @available(OpenSwiftUI_v6_0, *) + public private(set) var isTruncated: Bool + + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public var truncated: Bool { + isTruncated + } + + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public private(set) var numberOfLines: Int + + @_alwaysEmitIntoClient + public var startIndex: Int { 0 } + + public var endIndex: Int { lines.endIndex } + + public subscript(index: Int) -> Text.Layout.Line { lines[index] } + + // MARK: - Text.Layout.CharacterIndex + + /// The index of a character in the source text. An opaque + /// type, this is intended to be used to determine relative + /// locations of elements in the layout, rather than how they + /// map to the source strings. + @frozen + public struct CharacterIndex: Comparable, Hashable, Strideable, Sendable { + @usableFromInline + package var value: Int + + @_alwaysEmitIntoClient + internal init(value: Int) { + self.value = value + } + + @_alwaysEmitIntoClient + public static func < (lhs: Text.Layout.CharacterIndex, rhs: Text.Layout.CharacterIndex) -> Bool { + 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 + } + } + + // MARK: - Text.Layout.TypographicBounds + + /// The typographic bounds of an element in a text layout. + @frozen + public struct TypographicBounds: Equatable, Sendable { + /// The position of the left edge of the element's + /// baseline, relative to the text view. + public var origin: CGPoint + + /// The width of the element. + public var width: CGFloat + + /// The ascent of the element. + public var ascent: CGFloat + + /// The descent of the element. + public var descent: CGFloat + + /// The leading of the element. + public var leading: CGFloat + + /// Initializes to an empty bounds with zero origin. + @_alwaysEmitIntoClient + public init() { + origin = .init() + (width, ascent, descent, leading) = (0, 0, 0, 0) + } + + /// Returns a rectangle encapsulating the bounds. + @_alwaysEmitIntoClient + public var rect: CGRect { + CGRect( + x: origin.x, + y: origin.y - ascent, + width: width, + height: ascent + descent, + ) + } + } + + // MARK: - Text.Layout.Line [WIP] + + /// A single line in a text layout: a collection of runs of + /// placed glyphs. + public struct Line: RandomAccessCollection, Equatable { + /// The origin of the line. + public var origin: CGPoint + + package var drawingOptions: Text.Layout.DrawingOptions + + @_alwaysEmitIntoClient + public var startIndex: Int { + 0 + } + + public var endIndex: Int { + _openSwiftUIUnimplementedFailure() + } + + public subscript(index: Int) -> Text.Layout.Run { + _openSwiftUIUnimplementedFailure() + } + + public var typographicBounds: Text.Layout.TypographicBounds { + _openSwiftUIUnimplementedFailure() + } + + @_spi(Private) + @available(OpenSwiftUI_v6_0, *) + public var characterRange: Range { + _openSwiftUIUnimplementedFailure() + } + +// package func characterRanges(runIndices: Range) -> _RangeSet { +// _openSwiftUIUnimplementedFailure() +// } +// +// package func characterRanges(runIndices: _RangeSet) -> _RangeSet { +// _openSwiftUIUnimplementedFailure() +// } + + @_spi(Private) + @available(OpenSwiftUI_v6_0, *) + public var paragraphLayoutDirection: LayoutDirection { + _openSwiftUIUnimplementedFailure() + } + } + + // MARK: - Text.Layout.Run [WIP] + + public struct Run: RandomAccessCollection, Equatable { + @_spi(Private) + @available(OpenSwiftUI_v6_0, *) + public var lineOrigin: CGPoint + + @_spi(_) + @available(*, deprecated, renamed: "lineOrigin") + public var origin: CGPoint { + lineOrigin + } + + @_alwaysEmitIntoClient + public var startIndex: Int { + 0 + } + + public var endIndex: Int { + _openSwiftUIUnimplementedFailure() + } + + @_alwaysEmitIntoClient + public subscript(index: Int) -> Text.Layout.RunSlice { + self[index ..< index &+ 1] + } + + @_alwaysEmitIntoClient + public subscript(bounds: Range) -> Text.Layout.RunSlice { + RunSlice(run: self, indices: bounds) + } + + public subscript(key: T.Type) -> T? where T: TextAttribute { + _openSwiftUIUnimplementedFailure() + } + + public var layoutDirection: LayoutDirection { + _openSwiftUIUnimplementedFailure() + } + + public var typographicBounds: Text.Layout.TypographicBounds { + _openSwiftUIUnimplementedFailure() + } + + public var characterIndices: [Text.Layout.CharacterIndex] { + _openSwiftUIUnimplementedFailure() + } + + @_spi(Private) + @available(OpenSwiftUI_v6_0, *) + public var characterRange: Range { + _openSwiftUIUnimplementedFailure() + } + } + + // MARK: - Text.Layout.RunSlice [WIP] + + /// A slice of a run of placed glyphs in a text layout. + public struct RunSlice: RandomAccessCollection, Equatable { + public var run: Text.Layout.Run + + public var indices: Range + + public init(run: Text.Layout.Run, indices: Range) { + self.run = run + self.indices = indices + } + + @_alwaysEmitIntoClient + public var startIndex: Int { + indices.lowerBound + } + + @_alwaysEmitIntoClient + public var endIndex: Int { + indices.upperBound + } + + @_alwaysEmitIntoClient + public subscript(index: Int) -> Text.Layout.RunSlice { + self[index ..< index &+ 1] + } + + public subscript(bounds: Range) -> Text.Layout.RunSlice { + _openSwiftUIUnimplementedFailure() + } + + @_alwaysEmitIntoClient + public subscript(key: T.Type) -> T? where T: TextAttribute { + run[key] + } + + public var typographicBounds: Text.Layout.TypographicBounds { + _openSwiftUIUnimplementedFailure() + } + + public var characterIndices: [Text.Layout.CharacterIndex] { + _openSwiftUIUnimplementedFailure() + } + } + } +} + +@available(*, unavailable) +extension Text.Layout: Sendable {} + +@available(*, unavailable) +extension Text.Layout.Line: Sendable {} + +@available(*, unavailable) +extension Text.Layout.Run: Sendable {} + +@available(*, unavailable) +extension Text.Layout.RunSlice: Sendable {} + +// TODO: AnyTextLayoutRenderer + +// MARK: - Text.LayoutKey + +@available(OpenSwiftUI_v5_0, *) +extension Text { + + /// A preference key that provides the `Text.Layout` values for all + /// text views in the queried subtree. + public struct LayoutKey: PreferenceKey, Sendable { + + public struct AnchoredLayout: Equatable { + + /// The origin of the text layout. + public var origin: Anchor + + /// The text layout value. + public var layout: Text.Layout + } + + public static let defaultValue: [AnchoredLayout] = [] + + public static func reduce( + value: inout [AnchoredLayout], + nextValue: () -> [AnchoredLayout] + ) { + value.append(contentsOf: nextValue()) + } + } +} + +@available(*, unavailable) +extension Text.LayoutKey.AnchoredLayout: Sendable {} + +// MARK: - Text.DrawingOptions + +@available(OpenSwiftUI_v5_0, *) +extension Text.Layout { + + /// Option flags used when drawing `Text.Layout` lines or runs into + /// a graphics context. + @frozen + public struct DrawingOptions: OptionSet { + public let rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + /// If set, subpixel quantization requested by the text engine + /// is disabled. This can be useful for text that will be + /// animated to prevent it jittering. + @_alwaysEmitIntoClient + public static var disablesSubpixelQuantization: Text.Layout.DrawingOptions { + .init(rawValue: 1 << 0) + } + } +} + +// TODO + +// MARK: - TextRendererInput + +struct TextRendererInput: ViewInput { + static let defaultValue: WeakAttribute = .init() +} + +extension _ViewInputs { + @inline(__always) + var textRenderer: WeakAttribute { + get { self[TextRendererInput.self] } + set { self[TextRendererInput.self] = newValue } + } +} + +// MARK: - TextRendererAddsDrawingGroupKey + +private struct TextRendererAddsDrawingGroupKey: EnvironmentKey { + static let defaultValue: Bool = false +} + +extension EnvironmentValues { + @inline(__always) + var textRendererAddsDrawingGroup: Bool { + get { self[TextRendererAddsDrawingGroupKey.self] } + set { self[TextRendererAddsDrawingGroupKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift index 59c70dff8..f2a104ac2 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+SizeFitting.swift @@ -151,3 +151,62 @@ extension TextSizeVariant: Codable { try container.encode(rawValue) } } + +// MARK: - SizeFittingTextResolver [WIP] + +protocol SizeFittingTextResolver: LayoutEngine { + associatedtype Input + associatedtype Engine: LayoutEngine + + func shouldUpdate(for input: Input, inputChanged: Bool) -> Bool + func value(for input: Input) -> SizeFittingTextCacheValue + var narrowerVariant: Self { get } +} + +protocol TextSizeFittingLogic { + func suggestedVariant(for proposedSize: _ProposedSize) -> TextSizeVariant? + func onInvalidation(of variant: TextSizeVariant) +} + +//extension ResolvedTextHelper: SizeFittingTextResolver { +// typealias Input = (text: Text?, env: EnvironmentValues, renderer: TextRendererBoxBase?) +// typealias Engine = StyledTextLayoutEngine +//} + +class SizeFittingTextCache { + +} + +struct SizeFittingTextCacheValue where Engine: LayoutEngine { + var text: ResolvedStyledText + var engine: Engine + var renderer: TextRendererBoxBase? +} + +// TODO + +// MARK: - _ViewInputs + VariantThatFitsFlag + +struct VariantThatFitsFlag: ViewInputBoolFlag {} + +extension _ViewInputs { + @inline(__always) + var variantThatFits: Bool { + get { self[VariantThatFitsFlag.self] } + set { self[VariantThatFitsFlag.self] = newValue } + } +} + +// MARK: - EnvironmentValues + textSizeVariant + +extension EnvironmentValues { + private struct TextSizeVariantKey: EnvironmentKey { + static let defaultValue: TextSizeVariant = .regular + } + + @inline(__always) + var textSizeVariant: TextSizeVariant { + get { self[TextSizeVariantKey.self] } + set { self[TextSizeVariantKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift index 2eb4baafc..f478180e2 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift @@ -104,10 +104,18 @@ private struct PreferTextLayoutManagerInputModifier: ViewInputsModifier { modifier: _GraphValue, inputs: inout _ViewInputs ) { - inputs[PreferTextLayoutManagerInput.self] = true + inputs.prefersTextLayoutManager = true } } package struct PreferTextLayoutManagerInput: ViewInput { package static var defaultValue: Bool { false } } + +extension _ViewInputs { + @inline(__always) + var prefersTextLayoutManager: Bool { + get { self[PreferTextLayoutManagerInput.self] } + set { self[PreferTextLayoutManagerInput.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Suffix.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Suffix.swift new file mode 100644 index 000000000..9d92dc030 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Suffix.swift @@ -0,0 +1,142 @@ +// +// Text+Suffix.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Blocked by ResolvedStyledText +// ID: 3A0E49913D84545BECD562BC22E4DF1C (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Text.Suffix + +@available(OpenSwiftUI_v5_0, *) +extension Text { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + public struct Suffix: Sendable, Equatable { + @_spi(Private) + package enum Storage: Equatable { + case automatic + case none + case truncated(Text) + case alwaysVisible(Text) + } + + package var storage: Text.Suffix.Storage + + public static var automatic: Text.Suffix { + .init(storage: .automatic) + } + + public static var none: Text.Suffix { + .init(storage: .none) + } + + public static func truncated(_ suffix: Text) -> Text.Suffix { + .init(storage: .truncated(suffix)) + } + + public static func alwaysVisible(_ suffix: Text) -> Text.Suffix { + .init(storage: .alwaysVisible(suffix)) + } + + package var text: Text? { + switch storage { + case .automatic, .none: nil + case let .truncated(text): text + case let .alwaysVisible(text): text + } + } + + func resolve(text: ResolvedStyledText?) -> ResolvedTextSuffix { + // TODO: ResolvedStyledText is not implemented yet + _openSwiftUIUnimplementedFailure() + } + } +} + +// MARK: - View + textSuffix + +@available(OpenSwiftUI_v5_0, *) +extension View { + @_spi(Private) + @available(OpenSwiftUI_v5_0, *) + nonisolated public func textSuffix(_ suffix: Text.Suffix) -> some View { + modifier(TextSuffixModifier(suffix: suffix)) + } +} + +// MARK: - ResolvedTextSuffix + +package enum ResolvedTextSuffix: Equatable { + case none + case truncated(Text.Layout.Line, [ShapeStyle.Pack.Style]) + case alwaysVisible(Text.Layout.Line, [ShapeStyle.Pack.Style]) + + package var line: Text.Layout.Line? { + switch self { + case .none: nil + case let .truncated(line, _): line + case let .alwaysVisible(line, _): line + } + } + + package var styles: [ShapeStyle.Pack.Style] { + switch self { + case .none: [] + case let .truncated(_, styles): styles + case let .alwaysVisible(_, styles): styles + } + } +} + +private struct TextSuffixModifier: PrimitiveViewModifier, _GraphInputsModifier { + var suffix: Text.Suffix + + static func _makeInputs( + modifier: _GraphValue, + inputs: inout _GraphInputs + ) { + let text = Attribute(OptionalText(modifier: modifier.value)) + let referenceDate = inputs.referenceDate + let archivedView = inputs.archivedView + let interfaceIdiom = inputs.interfaceIdiom + // TODO: helper + _openSwiftUIUnimplementedFailure() + } + + struct ChildEnvironment: Rule, AsyncAttribute { + @Attribute var suffix: ResolvedTextSuffix + @Attribute var environment: EnvironmentValues + + var value: EnvironmentValues { + var env = environment + env[TextSuffixKey.self] = suffix + return env + } + } + + struct ResolvedTextSuffixFilter: Rule, AsyncAttribute { + @Attribute var modifier: TextSuffixModifier + @Attribute var text: ResolvedStyledText? + + var value: ResolvedTextSuffix { + modifier.suffix.resolve(text: text) + } + } + + struct OptionalText: Rule, AsyncAttribute { + @Attribute var modifier: TextSuffixModifier + + var value: Text? { + modifier.suffix.text + } + } +} + +// MARK: - TextSuffixKey + +private struct TextSuffixKey: EnvironmentKey { + static let defaultValue: ResolvedTextSuffix = .none +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 6db001cbd..abf65f68d 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -6,14 +6,16 @@ // Status: WIP // ID: 641995D812913A47B866B20B88782376 (SwiftUICore) -import OpenAttributeGraphShims +package import Foundation +package import OpenAttributeGraphShims public import OpenCoreGraphicsShims +import UIFoundation_Private // MARK: - Text + View [WIP] @available(OpenSwiftUI_v1_0, *) extension Text: UnaryView, PrimitiveView { - public nonisolated static func _makeView( + nonisolated public static func _makeView( view: _GraphValue, inputs: _ViewInputs ) -> _ViewOutputs { @@ -28,11 +30,242 @@ extension Text: UnaryView, PrimitiveView { } } - private static func makeCommonAttributes( + nonisolated private static func makeCommonAttributes( view: _GraphValue, inputs: _ViewInputs ) -> _ViewOutputs { - .init() + var newInputs = inputs + let allowsSelection = false // TODO: TextAllowsSelection + let options = inputs.base.options + let textRenderer = inputs.textRenderer + var features: ResolvedProperties.Features = inputs.archivedView.isArchived ? [] : .useTextSuffix + if textRenderer.attribute != nil { + features.formUnion([.customRenderer, .produceTextLayout]) + } else { + if inputs.preferences.contains(Text.LayoutKey.self) { + features.formUnion(.produceTextLayout) + } + } + if inputs.prefersTextLayoutManager { + features.formUnion(.useTextLayoutManager) + } + let resolvedText: Attribute + if inputs.variantThatFits { + // TODO + _openSwiftUIUnimplementedFailure() + } else { + let helper = ResolvedTextHelper( + time: inputs.time, + referenceDate: inputs.referenceDate, + includeDefaultAttributes: true, + allowsKeyColors: true, + archiveOptions: inputs.archivedView, + features: features, + attachmentsAsAuxiliaryMetadata: inputs.hasWidgetMetadata, + idiom: inputs.base.interfaceIdiom, + lastText: nil, + nextUpdate: .time(.zero), + sizeVariant: .regular + ) + resolvedText = Attribute( + ResolvedTextFilter( + text: view.value, + environment: inputs.environment, + helper: helper + ) + ) + } + newInputs.base.options.formUnion(.doNotScrape) + var outputs: _ViewOutputs + if allowsSelection { + _openSwiftUIUnimplementedFailure() + } else { + outputs = makeTextChildQuery( + newInputs.textAccessibilityProvider, + styledText: resolvedText, + view: view.value, + render: textRenderer, + inputs: newInputs, + isScrapeable: inputs.isScrapeable + ) + } + if let textAlwaysOnProvider = inputs.textAlwaysOnProvider { + textAlwaysOnProvider.makeAlwaysOn( + inputs: inputs, + schedule: resolvedText.schedule, + outputs: &outputs + ) + } + // FIXME + return outputs + } + + nonisolated private static func makeTextChildQuery

( + _ provider: P.Type, + styledText: Attribute, + view: Attribute, + render: WeakAttribute, + inputs: _ViewInputs, + isScrapeable: Bool + ) -> _ViewOutputs where P: TextAccessibilityProvider { + let query = Attribute( + TextChildQuery

( + resolvedText: styledText, + unresolvedText: view, + renderer: render, + environment: inputs.environment, + position: inputs.position, + size: inputs.size, + transform: inputs.transform, + parentID: inputs.scrapeableParentID + ) + ) + if isScrapeable { + query.flags = .scrapeable + } + return TextChildQuery

.Value.makeDebuggableView( + view: .init(query), + inputs: inputs + ) + } + + // TODO + private struct MakeRepresentableContext {} +} + +// MARK: - AccessibilityStyledTextContentView + +package struct AccessibilityStyledTextContentView: View where Provider: TextAccessibilityProvider { + package var text: ResolvedStyledText + + package var unresolvedText: Text + + package var renderer: TextRendererBoxBase? + + package var needsDrawingGroup: Bool + + package init( + text: ResolvedStyledText, + unresolvedText: Text, + renderer: TextRendererBoxBase? = nil, + needsDrawingGroup: Bool = false + ) { + self.text = text + self.unresolvedText = unresolvedText + self.renderer = renderer + self.needsDrawingGroup = needsDrawingGroup + } + + package var body: some View { + Provider.makeView( + content: StyledTextContentView( + text: text, + renderer: renderer, + needsDrawingGroup: needsDrawingGroup + ), + text: unresolvedText, + resolved: text + ) + } +} + +// MARK: - StyledTextContentView [WIP] + +package struct StyledTextContentView: UnaryView, PrimitiveView, ShapeStyledLeafView { + package var text: ResolvedStyledText + package var renderer: TextRendererBoxBase? + package var needsDrawingGroup: Bool + + package init( + text: ResolvedStyledText, + renderer: TextRendererBoxBase? = nil, + needsDrawingGroup: Bool = false + ) { + self.text = text + self.renderer = renderer + self.needsDrawingGroup = needsDrawingGroup + } + + package static var animatesSize: Bool { + false + } + + package func shape(in size: CGSize) -> FramedShape { + var frame = CGRect(origin: .zero, size: size) + if let renderer { + frame = frame.outset(by: renderer.displayPadding) + } + let shape: ShapeStyle.RenderedShape.Shape = .text(self) + return (shape, frame) + } + + nonisolated package static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + var newInputs = inputs + if inputs.preferences.requiresViewResponders { + newInputs.preferences.requiresViewResponders = false + } + let shapeStyles = inputs.resolvedShapeStyles( + role: .stroke, + mode: nil + ) + var outputs: _ViewOutputs + if inputs.preferences.requiresDisplayList { + if inputs.archivedView.isArchived { + newInputs.environment = Attribute( + ArchivedTransitionEnvironment( + view: view.value, + environment: inputs.environment + ) + ) + // TODO: ContentTransition + _openSwiftUIUnimplementedWarning() + // FIXME + outputs = .init() + } else { + let group = _ShapeStyle_InterpolatorGroup() + if inputs.needsGeometry { + newInputs.position = inputs.animatedPosition() + } + outputs = makeLeafView( + view: view, + inputs: newInputs, + styles: shapeStyles, + interpolatorGroup: group + ) + // TODO: outputs.applyInterpolatorGroup + } + } else { + outputs = makeLeafView( + view: view, + inputs: newInputs, + styles: shapeStyles, + interpolatorGroup: nil + ) + } + if inputs.requestsLayoutComputer { + outputs.layoutComputer = Attribute( + StyledTextLayoutComputer(textView: view.value) + ) + } + // TODO: Text.Layout.Key + return outputs + } + + package typealias Body = Never + + package typealias ShapeUpdateData = Void + + private struct ArchivedTransitionEnvironment: Rule, AsyncAttribute { + @Attribute var view: StyledTextContentView + @Attribute var environment: EnvironmentValues + + var value: EnvironmentValues { + _openSwiftUIUnimplementedWarning() + return environment + } } } @@ -195,6 +428,8 @@ public struct TextLayoutProperties: Equatable { @available(*, unavailable) extension TextLayoutProperties: Sendable {} +// MARK: - TextLayoutProperties + ProtobufMessage [TODO] + @_spi(Private) extension TextLayoutProperties: ProtobufMessage { package func encode(to encoder: inout ProtobufEncoder) throws { @@ -206,6 +441,387 @@ extension TextLayoutProperties: ProtobufMessage { } } +// MARK: - ResolvedStyledText [WIP] + +@available(OpenSwiftUI_v6_0, *) +@usableFromInline +package class ResolvedStyledText: CustomStringConvertible { + final package var layoutProperties: TextLayoutProperties + + final package var layoutMargins: EdgeInsets + + final package var scaleFactorOverride: CGFloat? { + didSet { + // TODO + _openSwiftUIUnimplementedWarning() + } + } + + package func resetCache() { + _openSwiftUIUnimplementedFailure() + } + + final package let storage: NSAttributedString? + + final package let stylePadding: EdgeInsets + + package var drawingMargins: EdgeInsets { + _openSwiftUIUnimplementedFailure() + } + + final package let isCollapsible: Bool + + final package let features: Text.ResolvedProperties.Features + + final package let styles: [_ShapeStyle_Pack.Style] + + final package let transitions: [Text.ResolvedProperties.Transition] + + final package var isDynamic: Bool { + _openSwiftUIUnimplementedFailure() + } + + final package var isEmpty: Bool { + _openSwiftUIUnimplementedFailure() + } + + final package var needsStyledRendering: Bool { + _openSwiftUIUnimplementedFailure() + } + + final package var needsRBDisplayList: Bool { + _openSwiftUIUnimplementedFailure() + } + + final package var maxFontMetrics: NSAttributedString.EncodedFontMetrics { + _openSwiftUIUnimplementedFailure() + } + + var schedule: (any TimelineSchedule)? { + _openSwiftUIUnimplementedWarning() + return nil + } + + package init( + storage: NSAttributedString?, + layoutProperties: TextLayoutProperties, + layoutMargins: EdgeInsets?, + stylePadding: EdgeInsets, + archiveOptions: ArchivedViewInput.Value, + isCollapsible: Bool, + features: Text.ResolvedProperties.Features, + suffix: ResolvedTextSuffix, + attachments: Text.ResolvedProperties.CustomAttachments, + styles: [_ShapeStyle_Pack.Style], + transitions: [Text.ResolvedProperties.Transition], + scaleFactorOverride: CGFloat? + ) { + _openSwiftUIUnimplementedFailure() + } + + package func lineHeightScalingAdjustment( + lineHeightMultiple: CGFloat, + maximumLineHeight: CGFloat, + minimumLineHeight: CGFloat + ) -> CGFloat { + _openSwiftUIUnimplementedFailure() + } + + final package func draw( + in drawingArea: CGRect, + with measuredSize: CGSize, + applyingMarginOffsets: Bool = true, + context: TextDrawingContext = .shared, + renderer: TextRendererBoxBase? = nil + ) { + _openSwiftUIUnimplementedFailure() + } + + package var majorAxis: Axis { + _openSwiftUIUnimplementedFailure() + } + + package func drawingScale(size: CGSize) -> CGFloat { + _openSwiftUIUnimplementedFailure() + } + + package func spacing() -> Spacing { + _openSwiftUIUnimplementedFailure() + } + + package func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize { + _openSwiftUIUnimplementedFailure() + } + + package func size(in request: CGSize) -> CGSize { + _openSwiftUIUnimplementedFailure() + } + + package func frameSize(in request: CGSize) -> CGSize { + _openSwiftUIUnimplementedFailure() + } + + package func size( + in request: CGSize, + context: TextDrawingContext + ) -> CGSize { + _openSwiftUIUnimplementedFailure() + } + + package func explicitAlignment( + _ k: AlignmentKey, + at size: CGSize + ) -> CGFloat? { + _openSwiftUIUnimplementedFailure() + } + + package func linkURL( + at point: CGPoint, + in size: CGSize + ) -> URL? { + _openSwiftUIUnimplementedFailure() + } + + package func draw( + in drawingArea: CGRect, + with measuredSize: CGSize, + applyingMarginOffsets: Bool, + containsResolvable: Bool, + context: TextDrawingContext, + renderer: TextRendererBoxBase? = nil + ) { + _openSwiftUIUnimplementedFailure() + } + + package func layoutValue( + in drawingArea: CGRect, + with measuredSize: CGSize, + applyingMarginOffsets: Bool = true + ) -> Text.Layout? { + _openSwiftUIUnimplementedFailure() + } + + final package func resolvedContent(in context: ResolvableStringResolutionContext) -> NSAttributedString? { + _openSwiftUIUnimplementedFailure() + } + + final package func resolvingContent(in context: ResolvableStringResolutionContext) -> ResolvedStyledText { + _openSwiftUIUnimplementedFailure() + } + + final package func nextUpdate( + after time: Time, + equivalentDate date: Date, + reduceFrequency: Bool = false + ) -> Time { + _openSwiftUIUnimplementedFailure() + } + +// final package var updatesAsynchronously: Bool { +// _openSwiftUIUnimplementedFailure() +// } + + @usableFromInline + final package var description: String { + _openSwiftUIUnimplementedFailure() + } + + final package var accessibilityText: Text { + _openSwiftUIUnimplementedFailure() + } + +// final package var cgStyleHandler: RBCGStyleHandler? { +// _openSwiftUIUnimplementedFailure() +// } + + final package func makeRBDisplayList( + for size: CGSize, + renderer: TextRendererBoxBase?, + deviceScale: CGFloat + ) -> any RBDisplayListContents { + _openSwiftUIUnimplementedFailure() + } +} + +extension ResolvedStyledText { + package func textSizeCacheMetrics(in size: CGSize) -> (UInt?, CGSize) { + _openSwiftUIUnimplementedFailure() + } + + package func linkURLMetrics( + in size: CGSize, + layoutMargins: EdgeInsets + ) -> CGFloat { + _openSwiftUIUnimplementedFailure() + } +} + +@available(*, unavailable) +extension ResolvedStyledText: Sendable {} + +extension ResolvedStyledText { + package func firstBaseline(in size: CGSize) -> CGFloat { + _openSwiftUIUnimplementedFailure() + } + + package func lastBaseline(in size: CGSize) -> CGFloat { + _openSwiftUIUnimplementedFailure() + } + + package func frame(in request: CGSize) -> CGRect { + _openSwiftUIUnimplementedFailure() + } + + package func frameOffset() -> CGSize { + _openSwiftUIUnimplementedFailure() + } +} + +extension ResolvedStyledText { + // FIXME + package class StringDrawing {} +} + +// MARK: - TextDrawingContext + +#if !canImport(Darwin) +class NSStringDrawingContext {} +#endif + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +final public class TextDrawingContext { + @AtomicBox + var ctx: NSStringDrawingContext + + init(ctx: NSStringDrawingContext) { + self.ctx = ctx + } + + static let shared: TextDrawingContext = { + let ctx = NSStringDrawingContext() + #if canImport(Darwin) + ctx.wrapsForTruncationMode = true + ctx.wantsBaselineOffset = true + ctx.wantsScaledLineHeight = true + ctx.wantsScaledBaselineOffset = true + ctx.cachesLayout = true + #endif + return TextDrawingContext(ctx: ctx) + }() +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension TextDrawingContext: Sendable {} + +// MARK: - ResolvedStyledText + Extension [WIP] + +//extension ResolvedStyledText { +// package static func styledText( +// storage: NSAttributedString?, +// layoutProperties: TextLayoutProperties, +// layoutMargins: EdgeInsets?, +// stylePadding: EdgeInsets, +// archiveOptions: ArchivedViewInput.Value, +// isCollapsible: Bool, +// features: Text.ResolvedProperties.Features, +// suffix: ResolvedTextSuffix, +// attachments: Text.ResolvedProperties.CustomAttachments, +// styles: [_ShapeStyle_Pack.Style], +// transitions: [Text.ResolvedProperties.Transition], +// scaleFactorOverride: CGFloat?, +// isInitialResolution: Bool = true +// ) -> ResolvedStyledText +// +// package static func styledText( +// storage: NSAttributedString?, +// stylePadding: EdgeInsets = EdgeInsets(), +// layoutProperties: TextLayoutProperties, +// archiveOptions: ArchivedViewInput.Value = .init(), +// isCollapsible: Bool = false, +// features: Text.ResolvedProperties.Features = .init(), +// suffix: ResolvedTextSuffix = .none, +// attachments: Text.ResolvedProperties.CustomAttachments = .init(), +// styles: [_ShapeStyle_Pack.Style] = .init(), +// transitions: [Text.ResolvedProperties.Transition] = .init() +// ) -> ResolvedStyledText +// +// package static func styledText( +// storage: NSAttributedString?, +// stylePadding: EdgeInsets = EdgeInsets(), +// environment: EnvironmentValues, +// archiveOptions: ArchivedViewInput.Value = .init(), +// isCollapsible: Bool = false, +// features: Text.ResolvedProperties.Features = .init(), +// suffix: ResolvedTextSuffix = .none, +// attachments: Text.ResolvedProperties.CustomAttachments = .init(), +// styles: [_ShapeStyle_Pack.Style] = .init(), +// transitions: [Text.ResolvedProperties.Transition] = .init(), +// writingMode: Text.WritingMode? = nil, +// sizeFitting: Bool = false +// ) -> ResolvedStyledText +//} + +// MARK: - CodableResolvedStyledText [WIP] + +struct CodableResolvedStyledText: ProtobufMessage { + var base: ResolvedStyledText + + init(from decoder: inout ProtobufDecoder) throws { + _openSwiftUIUnimplementedFailure() + } + + func encode(to encoder: inout ProtobufEncoder) throws { + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - DynamicTextViewFactory + +struct DynamicTextViewFactory: DisplayList.ViewFactory { + var text: ResolvedStyledText + var size: CGSize + var identity: DisplayList.Identity + + func makeView() -> AnyView { + AnyView(DynamicTextView(text: text, size: size)) + } + + var viewType: any Any.Type { + DynamicTextView.self + } +} + +// MARK: - StyledTextLayoutComputer + +private struct StyledTextLayoutComputer: StatefulRule, AsyncAttribute { + @Attribute var textView: StyledTextContentView + + typealias Value = LayoutComputer + + mutating func updateValue() { + let engine = StyledTextLayoutEngine( + text: textView.text, + renderer: textView.renderer + ) + update(to: engine) + } +} + +// MARK: - TextLayoutQuery [WIP] + +struct TextLayoutQuery { + var _resolvedText: Attribute + var _position: Attribute + var _size: Attribute + var _transform: Attribute + + var value: [Text.LayoutKey.AnchoredLayout] { + _openSwiftUIUnimplementedFailure() + } +} + // MARK: - ResolvedTextFilter [WIP] struct ResolvedTextFilter: StatefulRule, AsyncAttribute { @@ -216,57 +832,167 @@ struct ResolvedTextFilter: StatefulRule, AsyncAttribute { typealias Value = ResolvedStyledText func updateValue() { - ResolvedStyledText() + _openSwiftUIUnimplementedFailure() } } +// MARK: - ResolvedTextHelper [WIP] + struct ResolvedTextHelper { + enum NextUpdate { + case time(Time) + case recipe(lastTime: Time, lastDate: Date, reduceFrequency: Bool, resolved: ResolvedStyledText) + case none + } -} + @Attribute var time: Time + @WeakAttribute var referenceDate: Date?? + let includeDefaultAttributes: Bool + let allowsKeyColors: Bool + let archiveOptions: ArchivedViewInput.Value + let features: Text.ResolvedProperties.Features + let attachmentsAsAuxiliaryMetadata: Bool + let idiom: AnyInterfaceIdiom + let tracker: PropertyList.Tracker + var lastText: Text? + var nextUpdate: ResolvedTextHelper.NextUpdate + var sizeVariant: TextSizeVariant -package class TextRendererBoxBase {} + init( + time: Attribute

: Rule, AsyncAttribute, ScrapeableAttribute where P: TextAccessibilityProvider { + @Attribute var resolvedText: ResolvedStyledText + @Attribute var unresolvedText: Text + @WeakAttribute var renderer: TextRendererBoxBase? + @Attribute var environment: EnvironmentValues + @Attribute var position: CGPoint + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + let parentID: ScrapeableID - package var needsDrawingGroup: Bool + static func scrapeContent(from ident: AnyAttribute) -> ScrapeableContent.Item? { + let query = ident.info.body.assumingMemoryBound(to: TextChildQuery.self)[] + return ScrapeableContent.Item( + .text(query.unresolvedText, query.resolvedText, query.environment), + ids: .none, + query.parentID, + position: query.$position, + size: query.$size, + transform: query.$transform + ) + } - package init( - text: ResolvedStyledText, - unresolvedText: Text, - renderer: TextRendererBoxBase? = nil, - needsDrawingGroup: Bool = false - ) { - _openSwiftUIUnimplementedFailure() + var value: some View { + let accessibilityView = AccessibilityStyledTextContentView

( + text: resolvedText, + unresolvedText: unresolvedText, + renderer: renderer, + needsDrawingGroup: renderer != nil ? environment.textRendererAddsDrawingGroup : false + ) + return accessibilityView.body } +} - package var body: some View { +struct ResolvedOptionalTextFilter { + var _text: Attribute> + var _environment: Attribute + var helper: ResolvedTextHelper + + func updateValue() { _openSwiftUIUnimplementedFailure() } } -// FIXME: +// MARK: - DynamicTextView [WIP] -@available(OpenSwiftUI_v6_0, *) -@usableFromInline -package class ResolvedStyledText: CustomStringConvertible { - @usableFromInline - package var description: String { +struct DynamicTextView: PrimitiveView, UnaryView { + var text: ResolvedStyledText + var size: CGSize + + static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { _openSwiftUIUnimplementedFailure() } } -extension ResolvedStyledText { - package class StringDrawing {} -} +// MARK: - StyledTextLayoutEngine + +struct StyledTextLayoutEngine: LayoutEngine { + var text: ResolvedStyledText + var renderer: TextRendererBoxBase? + + func spacing() -> Spacing { + text.spacing() + } + func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize { + if let renderer { + return renderer.sizeThatFits( + proposal: .init(proposedSize), + text: .init(text: text) + ) + } else { + guard proposedSize != .zero else { + return .zero + } + return text.sizeThatFits(proposedSize) + } + } -private struct TextFilter { + func lengthThatFits(_ proposal: _ProposedSize, in axis: Axis) -> CGFloat { + if axis == .horizontal, proposal.width == .zero { + return .zero + } + return sizeThatFits(proposal)[axis] + } + + func explicitAlignment(_ k: AlignmentKey, at viewSize: ViewSize) -> CGFloat? { + text.explicitAlignment(k, at: viewSize.value) + } + var debugContentDescription: String? { + text.storage?.string + } } // FIXME diff --git a/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift new file mode 100644 index 000000000..17db21fcd --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/TextNonDarwinShims.swift @@ -0,0 +1,16 @@ +// +// TextNonDarwinShims.swift +// OpenSwiftUICore + +#if !canImport(Darwin) +package import Foundation + +package class NSParagraphStyle {} +package class NSMutableParagraphStyle {} + +extension NSMutableAttributedString { + package var isEmptyOrTerminatedByParagraphSeparator: Bool { + false + } +} +#endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/CoreText/CoreText_Private.h b/Sources/OpenSwiftUI_SPI/Shims/CoreText/CoreText_Private.h index 1bfe5530b..2fb83c439 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/CoreText/CoreText_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/CoreText/CoreText_Private.h @@ -7,6 +7,7 @@ #ifndef OpenSwiftUI_SPI_CoreText_Private_h #define OpenSwiftUI_SPI_CoreText_Private_h +#include "Private/CTCompositionLanguage.h" #include "Private/CTFontLegibilityWeight.h" #include "Private/CoreTextSPI.h" diff --git a/Sources/OpenSwiftUI_SPI/Shims/CoreText/Private/CTCompositionLanguage.h b/Sources/OpenSwiftUI_SPI/Shims/CoreText/Private/CTCompositionLanguage.h new file mode 100644 index 000000000..7b9c2b14d --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/CoreText/Private/CTCompositionLanguage.h @@ -0,0 +1,26 @@ +// +// CTCompositionLanguage.h +// OpenSwiftUI_SPI + +#pragma once + +#include "OpenSwiftUIBase.h" + +#if __has_include() + +#import +#import + +CF_ASSUME_NONNULL_BEGIN + +typedef CF_ENUM(uint8_t, CTCompositionLanguage) { + kCTCompositionLanguageUnset, + kCTCompositionLanguageNone, + kCTCompositionLanguageJapanese, + kCTCompositionLanguageSimplifiedChinese, + kCTCompositionLanguageTraditionalChinese, +}; + +CF_ASSUME_NONNULL_END + +#endif /* CoreText.h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h index e0cb18beb..277ff7f1a 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h @@ -2,8 +2,7 @@ // NSAttributedString.h // OpenSwiftUI_SPI -#ifndef OpenSwiftUI_SPI_NSAttributedString_h -#define OpenSwiftUI_SPI_NSAttributedString_h +#pragma once #include "OpenSwiftUIBase.h" @@ -26,5 +25,20 @@ typedef OPENSWIFTUI_OPTIONS(NSInteger, NSUnderlineStyle) { NSUnderlineStyleByWord API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)) = 0x8000 } API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); +#if OPENSWIFTUI_TARGET_OS_DARWIN -#endif /* OpenSwiftUI_SPI_NSAttributedString_h */ +#import + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +@interface NSAttributedString (OpenSwiftUI_SPI) +- (NSAttributedString *)_ui_attributedSubstringFromRange_openswiftui_safe_wrapper:(NSRange)range scaledByScaleFactor:(CGFloat)factor OPENSWIFTUI_SWIFT_NAME(_ui_attributedSubstring(from:scaledBy:)); +@end + +@interface NSMutableAttributedString (OpenSwiftUI_SPI) +@property(readonly, assign, nonatomic) BOOL isEmptyOrTerminatedByParagraphSeparator_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(isEmptyOrTerminatedByParagraphSeparator); +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.m b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.m new file mode 100644 index 000000000..bf2454ca6 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.m @@ -0,0 +1,36 @@ +// +// NSAttributedString.m +// OpenSwiftUI_SPI + +#include "NSAttributedString.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import "../OpenSwiftUIShims.h" +#import +#import + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +@implementation NSAttributedString (OpenSwiftUI_SPI) + +- (NSAttributedString *)_ui_attributedSubstringFromRange_openswiftui_safe_wrapper:(NSRange)range scaledByScaleFactor:(CGFloat)factor { + OPENSWIFTUI_SAFE_WRAPPER_IMP(NSAttributedString *, @"_ui_attributedSubstringFromRange:scaledByScaleFactor", nil, NSRange, CGFloat); + return func(self, selector, range, factor); +} + +@end + +@implementation NSMutableAttributedString (OpenSwiftUI_SPI) + +- (BOOL)isEmptyOrTerminatedByParagraphSeparator_openswiftui_safe_wrapper { + OPENSWIFTUI_SAFE_WRAPPER_IMP(BOOL, @"isEmptyOrTerminatedByParagraphSeparator:scaledByScaleFactor", false); + return func(self, selector); +} + +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif + diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSCompositionLanguage.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSCompositionLanguage.h new file mode 100644 index 000000000..e1d05c62f --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSCompositionLanguage.h @@ -0,0 +1,20 @@ +// +// NSCompositionLanguage.h +// OpenSwiftUI_SPI + +#pragma once + +#import "OpenSwiftUIBase.h" + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +typedef OPENSWIFTUI_ENUM(uint8_t, NSCompositionLanguage) +{ + NSCompositionLanguageUnset, + NSCompositionLanguageNone, + NSCompositionLanguageJapanese, + NSCompositionLanguageSimplifiedChinese, + NSCompositionLanguageTraditionalChinese, +}; + +OPENSWIFTUI_ASSUME_NONNULL_END diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h new file mode 100644 index 000000000..9ee221e13 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSParagraphStyle.h @@ -0,0 +1,159 @@ +// +// NSParagraphStyle.h +// OpenSwiftUI_SPI + +#pragma once + +#import "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +// Modified based on iOS 18.5 SDK + +// NSParagraphStyle.h +// UIKit +// +// Copyright (c) 2011-2024, Apple Inc. All rights reserved. +// +// NSParagraphStyle and NSMutableParagraphStyle hold paragraph style information +// NSTextTab holds information about a single tab stop + +#import +#import "NSText.h" + +@class NSTextList; + +NS_HEADER_AUDIT_BEGIN(nullability, sendability) + +#if !__NSPARAGRAPH_STYLE_SHARED_SECTION__ +#define __NSPARAGRAPH_STYLE_SHARED_SECTION__ 1 + +#if OPENSWIFTUI_TARGET_OS_OSX +typedef NS_ENUM(NSUInteger, NSLineBreakMode) { + NSLineBreakByWordWrapping = 0, // Wrap at word boundaries, default + NSLineBreakByCharWrapping, // Wrap at character boundaries + NSLineBreakByClipping, // Simply clip + NSLineBreakByTruncatingHead, // Truncate at head of line: "...wxyz" + NSLineBreakByTruncatingTail, // Truncate at tail of line: "abcd..." + NSLineBreakByTruncatingMiddle // Truncate middle of line: "ab...yz" +} API_AVAILABLE(macos(10.0), ios(6.0), watchos(2.0), tvos(9.0), visionos(1.0)); +#else +typedef NS_ENUM(NSInteger, NSLineBreakMode) { + NSLineBreakByWordWrapping = 0, // Wrap at word boundaries, default + NSLineBreakByCharWrapping, // Wrap at character boundaries + NSLineBreakByClipping, // Simply clip + NSLineBreakByTruncatingHead, // Truncate at head of line: "...wxyz" + NSLineBreakByTruncatingTail, // Truncate at tail of line: "abcd..." + NSLineBreakByTruncatingMiddle // Truncate middle of line: "ab...yz" +} API_AVAILABLE(macos(10.0), ios(6.0), watchos(2.0), tvos(9.0), visionos(1.0)); +#endif + +// Line break strategy describes a collection of options that can affect where line breaks are placed in a paragraph. +// This is independent from line break mode, which describes what happens when text is too long to fit within its container. +// These options won't have any effect when used with line break modes that don't support multiple lines, like clipping or truncating middle. +typedef NS_OPTIONS(NSUInteger, NSLineBreakStrategy) { + // Don't use any line break strategies + NSLineBreakStrategyNone = 0, + // Use the push out line break strategy. + // This strategy allows the text system to "push out" individual lines by some number of words to avoid an orphan word on the last line of the paragraph. + // The current implementation usually pushes out the last line by a single word. + NSLineBreakStrategyPushOut API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)) = 1 << 0, + // When specified, it prohibits breaking between Hangul characters. It is the preferable typesetting strategy for the modern Korean documents suitable for UI strings. + NSLineBreakStrategyHangulWordPriority API_AVAILABLE(macos(11.0), ios(14.0), watchos(7.0), tvos(14.0), visionos(1.0)) = 1 << 1, + // Use the same configuration of line break strategies that the system uses for standard UI labels. This set of line break strategies is optimized for displaying shorter strings that are common in UI labels and may not be suitable for large amounts of text. + NSLineBreakStrategyStandard API_AVAILABLE(macos(11.0), ios(14.0), watchos(7.0), tvos(14.0), visionos(1.0)) = 0xFFFF +} API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); + +#endif // !__NSPARAGRAPH_STYLE_SHARED_SECTION__ + +// NSTextTab +typedef NSString * NSTextTabOptionKey NS_TYPED_ENUM API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +OPENSWIFTUI_EXPORT NSTextTabOptionKey NSTabColumnTerminatorsAttributeName API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); // An attribute for NSTextTab options. The value is NSCharacterSet. The character set is used to determine the tab column terminating character. The tab and newline characters are implied even if not included in the character set. + +OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)) +@interface NSTextTab : NSObject + ++ (NSCharacterSet *)columnTerminatorsForLocale:(nullable NSLocale *)aLocale API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); // Returns the column terminators for locale. Passing nil returns an instance corresponding to +[NSLocale systemLocale]. For matching user's formatting preferences, pass +[NSLocale currentLocale]. Can be used as the value for NSTabColumnTerminatorsAttributeName to make a decimal tab stop. + +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat location; // Location of the tab stop inside the line fragment rect coordinate system +@property (readonly, NS_NONATOMIC_IOSONLY) NSDictionary *options; // Optional configuration attributes +@end + + +// NSParagraphStyle +OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)) +@interface NSParagraphStyle : NSObject + +@property (class, readonly, copy, NS_NONATOMIC_IOSONLY) NSParagraphStyle *defaultParagraphStyle; // This class property returns a shared and cached NSParagraphStyle instance with the default style settings, with same value as the result of [[NSParagraphStyle alloc] init]. + ++ (NSWritingDirection)defaultWritingDirectionForLanguage:(nullable NSString *)languageName; // languageName is in ISO lang region format + +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat lineSpacing; // "Leading": distance between the bottom of one line fragment and top of next (applied between lines in the same container). This value is included in the line fragment heights in layout manager. +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat paragraphSpacing; // Distance between the bottom of this paragraph and top of next (or the beginning of its paragraphSpacingBefore, if any). + +// The following values are relative to the appropriate margin (depending on the paragraph direction) + +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat headIndent; // Distance from margin to front edge of paragraph +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat tailIndent; // Distance from margin to back edge of paragraph; if negative or 0, from other margin +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat firstLineHeadIndent; // Distance from margin to edge appropriate for text direction + +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat minimumLineHeight; // Line height is the distance from bottom of descenders to top of ascenders; basically the line fragment height. Does not include lineSpacing (which is added after this computation). +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat maximumLineHeight; // 0 implies no maximum. + +@property (readonly, NS_NONATOMIC_IOSONLY) NSLineBreakMode lineBreakMode; + +@property (readonly, NS_NONATOMIC_IOSONLY) NSWritingDirection baseWritingDirection; + +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat lineHeightMultiple; // Natural line height is multiplied by this factor (if positive) before being constrained by minimum and maximum line height. +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat paragraphSpacingBefore; // Distance between the bottom of the previous paragraph (or the end of its paragraphSpacing, if any) and the top of this paragraph. + +// Specifies the threshold for hyphenation. Valid values lie between 0.0 and 1.0 inclusive. Hyphenation will be attempted when the ratio of the text width as broken without hyphenation to the width of the line fragment is less than the hyphenation factor. When this takes on its default value of 0.0, the layout manager's hyphenation factor is used instead. When both are 0.0, hyphenation is disabled. +@property (readonly, NS_NONATOMIC_IOSONLY) float hyphenationFactor; + +// A property controlling the hyphenation behavior for the paragraph associated with the paragraph style. The exact hyphenation logic is dynamically determined by the layout context such as language, platform, etc. When YES, it affects the return value from -hyphenationFactor when the property is set to 0.0. +@property (readonly, NS_NONATOMIC_IOSONLY) BOOL usesDefaultHyphenation API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)); + +@property (readonly,copy, NS_NONATOMIC_IOSONLY) NSArray *tabStops API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); // An array of NSTextTabs. Contents should be ordered by location. The default value is an array of 12 left-aligned tabs at 28pt interval +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat defaultTabInterval API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); // The default tab interval used for locations beyond the last element in tabStops + +@property (readonly, copy, NS_NONATOMIC_IOSONLY) NSArray *textLists API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); // Array to specify the text lists containing the paragraph, nested from outermost to innermost. + +@property (readonly, NS_NONATOMIC_IOSONLY) BOOL allowsDefaultTighteningForTruncation API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); // Tightens inter-character spacing in attempt to fit lines wider than the available space if the line break mode is one of the truncation modes before starting to truncate. NO by default. The maximum amount of tightening performed is determined by the system based on contexts such as font, line width, etc. + +@property (readonly, NS_NONATOMIC_IOSONLY) NSLineBreakStrategy lineBreakStrategy API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); // Specifies the line break strategies that may be used for laying out the paragraph. The default value is NSLineBreakStrategyNone. + +@end + + +OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)) +@interface NSMutableParagraphStyle : NSParagraphStyle + +@property (NS_NONATOMIC_IOSONLY) CGFloat lineSpacing; +@property (NS_NONATOMIC_IOSONLY) CGFloat paragraphSpacing; +@property (NS_NONATOMIC_IOSONLY) CGFloat firstLineHeadIndent; +@property (NS_NONATOMIC_IOSONLY) CGFloat headIndent; +@property (NS_NONATOMIC_IOSONLY) CGFloat tailIndent; +@property (NS_NONATOMIC_IOSONLY) NSLineBreakMode lineBreakMode; +@property (NS_NONATOMIC_IOSONLY) CGFloat minimumLineHeight; +@property (NS_NONATOMIC_IOSONLY) CGFloat maximumLineHeight; +@property (NS_NONATOMIC_IOSONLY) NSWritingDirection baseWritingDirection; +@property (NS_NONATOMIC_IOSONLY) CGFloat lineHeightMultiple; +@property (NS_NONATOMIC_IOSONLY) CGFloat paragraphSpacingBefore; +@property (NS_NONATOMIC_IOSONLY) float hyphenationFactor; +@property (readwrite, NS_NONATOMIC_IOSONLY) BOOL usesDefaultHyphenation API_AVAILABLE(macos(12.0), ios(15.0), tvos(15.0), watchos(8.0), visionos(1.0)); +@property (null_resettable, copy, NS_NONATOMIC_IOSONLY) NSArray *tabStops API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@property (NS_NONATOMIC_IOSONLY) CGFloat defaultTabInterval API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@property (NS_NONATOMIC_IOSONLY) BOOL allowsDefaultTighteningForTruncation API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@property (NS_NONATOMIC_IOSONLY) NSLineBreakStrategy lineBreakStrategy API_AVAILABLE(macos(10.11), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@property (NS_NONATOMIC_IOSONLY, copy) NSArray *textLists API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); + +- (void)addTabStop:(NSTextTab *)anObject API_AVAILABLE(macos(10.0), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (void)removeTabStop:(NSTextTab *)anObject API_AVAILABLE(macos(10.0), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); + +- (void)setParagraphStyle:(NSParagraphStyle *)obj API_AVAILABLE(macos(10.0), ios(9.0), tvos(9.0), watchos(2.0), visionos(1.0)); + +@end + +NS_HEADER_AUDIT_END(nullability, sendability) + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing.h new file mode 100644 index 000000000..543d2aec8 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing.h @@ -0,0 +1,83 @@ +// +// NSStringDrawing.h +// OpenSwiftUI_SPI + +#pragma once + +#import "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +// Modified based on iOS 18.5 SDK + +// +// NSStringDrawing.h +// UIKit +// +// Copyright (c) 2011-2024, Apple Inc. All rights reserved. +// + +#import +@class NSAttributedString; +@class NSString; +@class NSStringDrawingContext; + +NS_HEADER_AUDIT_BEGIN(nullability, sendability) + +// When attributes=nil, the methods declared here uses the default behavior for each attribute described in . When stringDrawingContext=nil, it's equivalent of passing the default instance initialized with [[NSStringDrawingContext alloc] init]. + +OPENSWIFTUI_EXPORT API_AVAILABLE(macos(10.11), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)) +@interface NSStringDrawingContext : NSObject + +// Minimum scale factor for drawWithRect:options:context: and boundingRectWithSize:options:context: methods. If this property is set, the extended string drawing methods will attempt to draw the attributed string in the given bounds by proportionally scaling the font(s) in the attributed string +@property (NS_NONATOMIC_IOSONLY)CGFloat minimumScaleFactor; + +// actual scale factor used by the last drawing call where minimum scale factor was specified +@property (readonly, NS_NONATOMIC_IOSONLY) CGFloat actualScaleFactor; + +// bounds of the string drawn by the previous invocation of drawWithRect:options:context: +@property (readonly, NS_NONATOMIC_IOSONLY) CGRect totalBounds; + +@end + +@interface NSString(NSStringDrawing) +- (CGSize)sizeWithAttributes:(nullable NSDictionary *)attrs API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (void)drawAtPoint:(CGPoint)point withAttributes:(nullable NSDictionary *)attrs API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (void)drawInRect:(CGRect)rect withAttributes:(nullable NSDictionary *)attrs API_AVAILABLE(macos(10.0), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@end + +@interface NSAttributedString(NSStringDrawing) +- (CGSize)size API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (void)drawAtPoint:(CGPoint)point API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (void)drawInRect:(CGRect)rect API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@end + +typedef NS_OPTIONS(NSInteger, NSStringDrawingOptions) { + NSStringDrawingUsesLineFragmentOrigin = 1 << 0, // The specified origin is the line fragment origin, not the base line origin + NSStringDrawingUsesFontLeading = 1 << 1, // Uses the font leading for calculating line heights + NSStringDrawingUsesDeviceMetrics = 1 << 3, // Uses image glyph bounds instead of typographic bounds + NSStringDrawingTruncatesLastVisibleLine API_AVAILABLE(macos(10.5), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)) = 1 << 5, // Truncates and adds the ellipsis character to the last visible line if the text doesn't fit into the bounds specified. Ignored if NSStringDrawingUsesLineFragmentOrigin is not also set. + #if OPENSWIFTUI_TARGET_OS_OSX + NSStringDrawingDisableScreenFontSubstitution API_DEPRECATED("", macos(10.0,10.11)) = (1 << 2), + NSStringDrawingOneShot API_DEPRECATED("", macos(10.0,10.11)) = (1 << 4), + #endif +} +NS_SWIFT_NAME(NSString.DrawingOptions) +API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); + + +// NOTE: All of the following methods will default to drawing on a baseline, limiting drawing to a single line. +// To correctly draw and size multi-line text, pass NSStringDrawingUsesLineFragmentOrigin in the options parameter. +@interface NSString (NSExtendedStringDrawing) +- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary *)attributes context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary *)attributes context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(7.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@end + +@interface NSAttributedString (NSExtendedStringDrawing) +- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); +- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); +@end + +NS_HEADER_AUDIT_END(nullability, sendability) + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing_Private.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing_Private.h new file mode 100644 index 000000000..6dbd3f07c --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing_Private.h @@ -0,0 +1,29 @@ +// +// NSStringDrawing_Private.h +// OpenSwiftUI_SPI + +#pragma once + +#import "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN +#import "NSStringDrawing.h" + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +@interface NSStringDrawingContext (OpenSwiftUI_SPI) +@property (nonatomic, assign) CGFloat baselineOffset; +@property (nonatomic, assign) CGFloat firstBaselineOffset; +@property (nonatomic, assign) BOOL wrapsForTruncationMode; +@property (nonatomic, assign) BOOL wantsBaselineOffset; +@property (nonatomic, assign) BOOL wantsScaledLineHeight; +@property (nonatomic, assign) BOOL wantsScaledBaselineOffset; +@property (nonatomic, assign) BOOL cachesLayout; +@end + +void _NSStringDrawingContextSetBaselineOffset(NSStringDrawingContext *context, CGFloat offset); +void _NSStringDrawingContextSetFirstBaselineOffset(NSStringDrawingContext *context, CGFloat offset); + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing_Private.m b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing_Private.m new file mode 100644 index 000000000..7323d48c6 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSStringDrawing_Private.m @@ -0,0 +1,17 @@ +// +// NSStringDrawing_Private.m +// OpenSwiftUI_SPI + +#import "NSStringDrawing_Private.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +void _NSStringDrawingContextSetBaselineOffset(NSStringDrawingContext *context, CGFloat offset) { + context.baselineOffset = offset; +} + +void _NSStringDrawingContextSetFirstBaselineOffset(NSStringDrawingContext *context, CGFloat offset) { + context.firstBaselineOffset = offset; +} + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSText.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSText.h new file mode 100644 index 000000000..165a2aa12 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSText.h @@ -0,0 +1,36 @@ +// +// NSText.h +// OpenSwiftUI_SPI + +#pragma once + +#import "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +// Modified based on macOS 15.5 SDK + +/* + NSText.h + Application Kit + Copyright (c) 1994-2024, Apple Inc. + All rights reserved. +*/ + +#import + +NS_HEADER_AUDIT_BEGIN(nullability, sendability) + +#if !__NSWRITING_DIRECTION_SHARED_SECTION__ +#define __NSWRITING_DIRECTION_SHARED_SECTION__ 1 +#pragma mark NSWritingDirection +typedef NS_ENUM(NSInteger, NSWritingDirection) { + NSWritingDirectionNatural = -1, // Determines direction using the Unicode Bidi Algorithm rules P2 and P3 + NSWritingDirectionLeftToRight = 0, // Left to right writing direction + NSWritingDirectionRightToLeft = 1 // Right to left writing direction +} API_AVAILABLE(macos(10.0), ios(6.0), watchos(2.0), tvos(9.0), visionos(1.0)); +#endif // !__NSWRITING_DIRECTION_SHARED_SECTION__ + +NS_HEADER_AUDIT_END(nullability, sendability) + +#endif