From bc8c274a0e6fd97c56fde40a805e86aa022f37a3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 00:28:09 +0800 Subject: [PATCH 01/16] Update ResolvedStyledText API --- .../Text+NSAttributedString.swift | 16 ++++++++ .../View/Text/Text/Text+View.swift | 37 ++++++++++++------- .../Text/Util/ReducedTimelineSchedule.swift | 11 +++++- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift index 44a9d8f50..15d5c53b5 100644 --- a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift +++ b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift @@ -107,6 +107,22 @@ extension NSAttributedString { package struct EncodedFontMetrics { package var capHeight: CGFloat, ascender: CGFloat, descender: CGFloat, leading: CGFloat package var outsets: EdgeInsets + + init(capHeight: CGFloat, ascender: CGFloat, descender: CGFloat, leading: CGFloat, outsets: EdgeInsets) { + self.capHeight = capHeight + self.ascender = ascender + self.descender = descender + self.leading = leading + self.outsets = outsets + } + + init() { + self.capHeight = .zero + self.ascender = .zero + self.descender = .zero + self.leading = .zero + self.outsets = .zero + } } package var maxFontMetrics: EncodedFontMetrics { diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index daa834ac6..54f677b8c 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -453,13 +453,12 @@ package class ResolvedStyledText: CustomStringConvertible { final package var scaleFactorOverride: CGFloat? { didSet { - // TODO - _openSwiftUIUnimplementedWarning() + resetCache() } } package func resetCache() { - _openSwiftUIUnimplementedFailure() + _openSwiftUIBaseClassAbstractMethod() } final package let storage: NSAttributedString? @@ -469,8 +468,7 @@ package class ResolvedStyledText: CustomStringConvertible { final package let archiveOptions: ArchivedViewInput.Value package var drawingMargins: EdgeInsets { - _openSwiftUIUnimplementedWarning() - return .zero + .zero } final package let isCollapsible: Bool @@ -481,8 +479,6 @@ package class ResolvedStyledText: CustomStringConvertible { final package let transitions: [Text.ResolvedProperties.Transition] - final private var _computedMaxFontMetrics: NSAttributedString.EncodedFontMetrics? - final package var isDynamic: Bool { guard let storage else { return false @@ -491,21 +487,36 @@ package class ResolvedStyledText: CustomStringConvertible { } final package var isEmpty: Bool { - _openSwiftUIUnimplementedFailure() + guard let storage else { + return true + } + return storage.length == 0 } final package var needsStyledRendering: Bool { - _openSwiftUIUnimplementedWarning() - return false + features.contains(.keyColor) || ( + features.contains(.attachments) && + archiveOptions.isArchived && + !isDynamic + ) } final package var needsRBDisplayList: Bool { - _openSwiftUIUnimplementedWarning() - return false + !isDynamic && ( + features.contains(.customRenderer) || + (archiveOptions.isArchived && archiveOptions.preciseTextLayout) + ) } + final private var _computedMaxFontMetrics: NSAttributedString.EncodedFontMetrics? + final package var maxFontMetrics: NSAttributedString.EncodedFontMetrics { - _openSwiftUIUnimplementedFailure() + if let _computedMaxFontMetrics { + return _computedMaxFontMetrics + } + let metrics = storage?.maxFontMetrics ?? .init() + _computedMaxFontMetrics = metrics + return metrics } final var schedule: (any TimelineSchedule)? { diff --git a/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift b/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift index 76e1e9d5a..77e7717ed 100644 --- a/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift +++ b/Sources/OpenSwiftUICore/View/Text/Util/ReducedTimelineSchedule.swift @@ -23,6 +23,15 @@ extension NSAttributedString { } var updateSchedule: any TimelineSchedule { - _openSwiftUIUnimplementedFailure() + guard length >= 1, + let schedule = attribute( + .updateSchedule, + at: 0, + effectiveRange: nil + ) as? any TimelineSchedule + else { + return ExplicitTimelineSchedule([]) + } + return schedule } } From 7d508c1d27a7c5ee55d44d1fa53294394d4235ed Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 19 Jul 2026 23:53:09 +0800 Subject: [PATCH 02/16] Update NSAttributedString API --- .../View/Text/Font/CoreText+Private.swift | 3 ++ .../View/Text/Text/Text+View.swift | 34 +++++++++++++++++++ .../Shims/UIFoundation/NSAttributedString.h | 4 +++ 3 files changed, 41 insertions(+) diff --git a/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift b/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift index d6f4146ea..9ef629220 100644 --- a/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift +++ b/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift @@ -57,6 +57,9 @@ package func CTFontDescriptorCreateForUIType( // MARK: - CTFont private API +@_silgen_name("CTFontCopySystemUIFontExcessiveLineHeightCharacterSet") +package func CTFontCopySystemUIFontExcessiveLineHeightCharacterSet() -> CFCharacterSet? + @_silgen_name("CTFontIsSystemUIFont") package func CTFontIsSystemUIFont(_ font: CTFont) -> Bool diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 54f677b8c..8210c32dd 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -11,6 +11,9 @@ package import OpenAttributeGraphShims public import OpenCoreGraphicsShims package import OpenRenderBoxShims import UIFoundation_Private +#if canImport(CoreText) +package import CoreText +#endif // MARK: - Text + View [WIP] @@ -442,6 +445,37 @@ extension TextLayoutProperties: ProtobufMessage { } } +#if canImport(CoreText) +extension NSAttributedString { + static let oversizedScalars: CharacterSet = { + guard let characterSet = CTFontCopySystemUIFontExcessiveLineHeightCharacterSet() else { + return CharacterSet() + } + return characterSet as CharacterSet + }() + + static let oversizedScalarsWithoutEmoji = oversizedScalars.subtracting( + NSCharacterSet.ic_emoji as CharacterSet + ) + + fileprivate func allFonts() -> Set { + var fonts: Set = [] + enumerateAttribute( + .kitFont, + in: NSRange(location: 0, length: length), + options: [] + ) { value, _, _ in + guard let value else { + return + } + let font = value as! CTFont + fonts.insert(font) + } + return fonts + } +} +#endif + // MARK: - ResolvedStyledText [WIP] @available(OpenSwiftUI_v6_0, *) diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h index 805d22d7d..584c818d7 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIFoundation/NSAttributedString.h @@ -64,6 +64,10 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN @property(readonly, assign, nonatomic) BOOL isEmptyOrTerminatedByParagraphSeparator_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(isEmptyOrTerminatedByParagraphSeparator); @end +@interface NSCharacterSet (OpenSwiftUI_SPI) +@property(class, readonly, nonatomic) NSCharacterSet *ic_emojiCharacterSet; +@end + OPENSWIFTUI_ASSUME_NONNULL_END #endif From bdf016d6e1cb48f3f0d7bb50e22552e2cdc04571 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 19 Jul 2026 23:53:25 +0800 Subject: [PATCH 03/16] Update Package.resolved --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 8c2de6b74..db99c086c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "13c69621d8a7bc34851161e1b06d0ef0e1304c504885cc1f439a9d38564c5f97", + "originHash" : "9ee38d6f53b74893089a214e320ca61727938f544556a98e447376e3d3f9f15e", "pins" : [ { "identity" : "darwinprivateframeworks", From 1bebbb24f8a8763b2514ce1c146a874d01aed7b8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 00:10:55 +0800 Subject: [PATCH 04/16] Implement NSAttributedString.maxFontMetrics --- .../Text+NSAttributedString.swift | 60 ++++++++++++++++++- .../View/Text/Font/CoreText+Private.swift | 16 +++++ .../View/Text/Font/CoreTextUtil.swift | 10 +--- .../NSAttributedStringTests.swift | 49 +++++++++++++++ 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 Tests/OpenSwiftUICoreTests/View/Text/AttributedString/NSAttributedStringTests.swift diff --git a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift index 15d5c53b5..54839eb32 100644 --- a/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift +++ b/Sources/OpenSwiftUICore/View/Text/AttributedString/Text+NSAttributedString.swift @@ -7,6 +7,9 @@ package import Foundation package import UIFoundation_Private +#if canImport(CoreText) +package import CoreText +#endif package func makeParagraphStyle(environment: EnvironmentValues) -> NSMutableParagraphStyle { let paragraphStyle = NSMutableParagraphStyle() @@ -126,7 +129,62 @@ extension NSAttributedString { } package var maxFontMetrics: EncodedFontMetrics { - _openSwiftUIUnimplementedFailure() + #if canImport(CoreText) + var capHeight: CGFloat = 0 + var ascender: CGFloat = 0 + var descender: CGFloat = 0 + var leading: CGFloat? + var outsets = EdgeInsets.zero + let hasOversizedScalars = string.rangeOfCharacter(from: Self.oversizedScalars) != nil + let usesTextRenderingMetrics = Semantics.TextRenderingMetrics.isEnabled + enumerateAttribute( + .kitFont, + in: NSRange(location: 0, length: length), + options: .longestEffectiveRangeNotRequired + ) { value, _, _ in + guard let value else { + return + } + let font = value as! CTFont + let fontAscender = font.ascender + let fontDescender = font.descender + capHeight = max(capHeight, font.capHeight) + ascender = max(ascender, fontAscender) + descender = max(descender, fontDescender) + leading = leading.map { max($0, font.leading) } ?? font.leading + if hasOversizedScalars || font.mayRequireLanguageAwareOutsets { + var left: CGFloat = 0 + var top: CGFloat = 0 + var right: CGFloat = 0 + var bottom: CGFloat = 0 + if CTFontGetLanguageAwareOutsets(font, &left, &top, &right, &bottom) { + outsets.top = max(outsets.top, top) + outsets.leading = max(outsets.leading, left) + outsets.bottom = max(outsets.bottom, bottom) + outsets.trailing = max(outsets.trailing, right) + return + } + } + if usesTextRenderingMetrics, !font.isSystemUIFont { + var clippingAscender = fontAscender + var clippingDescender = fontDescender + if CTFontGetClippingMetrics(font, &clippingAscender, &clippingDescender) { + outsets.top = max(outsets.top, max(0, clippingAscender - fontAscender)) + outsets.bottom = max(outsets.bottom, max(0, clippingDescender - fontDescender)) + } + } + } + return EncodedFontMetrics( + capHeight: capHeight, + ascender: ascender, + descender: -descender, + leading: leading ?? 0, + outsets: outsets + ) + #else + _openSwiftUIPlatformUnimplementedWarning() + return EncodedFontMetrics() + #endif } } diff --git a/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift b/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift index 9ef629220..9b88f8ee7 100644 --- a/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift +++ b/Sources/OpenSwiftUICore/View/Text/Font/CoreText+Private.swift @@ -60,6 +60,22 @@ package func CTFontDescriptorCreateForUIType( @_silgen_name("CTFontCopySystemUIFontExcessiveLineHeightCharacterSet") package func CTFontCopySystemUIFontExcessiveLineHeightCharacterSet() -> CFCharacterSet? +@_silgen_name("CTFontGetLanguageAwareOutsets") +package func CTFontGetLanguageAwareOutsets( + _ font: CTFont, + _ left: UnsafeMutablePointer, + _ top: UnsafeMutablePointer, + _ right: UnsafeMutablePointer, + _ bottom: UnsafeMutablePointer +) -> Bool + +@_silgen_name("CTFontGetClippingMetrics") +package func CTFontGetClippingMetrics( + _ font: CTFont, + _ ascent: UnsafeMutablePointer, + _ descent: UnsafeMutablePointer +) -> Bool + @_silgen_name("CTFontIsSystemUIFont") package func CTFontIsSystemUIFont(_ font: CTFont) -> Bool diff --git a/Sources/OpenSwiftUICore/View/Text/Font/CoreTextUtil.swift b/Sources/OpenSwiftUICore/View/Text/Font/CoreTextUtil.swift index 479442d90..5d88a1e50 100644 --- a/Sources/OpenSwiftUICore/View/Text/Font/CoreTextUtil.swift +++ b/Sources/OpenSwiftUICore/View/Text/Font/CoreTextUtil.swift @@ -49,7 +49,7 @@ extension CTFont { } package var stylisticClass: CTFontStylisticClass { - .init(rawValue: CTFontGetSymbolicTraits(self).rawValue & CTFontSymbolicTraits.traitClassMask.rawValue) + .init(rawValue: symbolicTraits.rawValue & CTFontSymbolicTraits.traitClassMask.rawValue) } package func scaled( @@ -85,14 +85,10 @@ extension CTFont { } package var mayRequireLanguageAwareOutsets: Bool { - guard symbolicTraits.isEmpty else { + guard !symbolicTraits.contains(.italicTrait), weight <= kCTFontWeightHeavy else { return true } - guard weight <= kCTFontWeightHeavy else { - return true - } - let stylisticClass = stylisticClass - return stylisticClass == [.modernSerifsClass] || stylisticClass == [.oldStyleSerifsClass, .modernSerifsClass] + return [CTFontStylisticClass.scriptsClass, .ornamentalsClass].contains(stylisticClass) } } diff --git a/Tests/OpenSwiftUICoreTests/View/Text/AttributedString/NSAttributedStringTests.swift b/Tests/OpenSwiftUICoreTests/View/Text/AttributedString/NSAttributedStringTests.swift new file mode 100644 index 000000000..2132885e3 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/View/Text/AttributedString/NSAttributedStringTests.swift @@ -0,0 +1,49 @@ +// +// NSAttributedStringTests.swift +// OpenSwiftUICoreTests +// + +#if canImport(CoreText) + +import CoreText +import Foundation +@testable import OpenSwiftUICore +import Testing + +struct NSAttributedStringTests { + @Test + func maxFontMetricsWithoutFontAttributes() { + let metrics = NSAttributedString(string: "OpenSwiftUI").maxFontMetrics + + #expect(metrics.capHeight == 0) + #expect(metrics.ascender == 0) + #expect(metrics.descender == 0) + #expect(metrics.leading == 0) + #expect(metrics.outsets == .zero) + } + + @Test + func maxFontMetricsAggregatesFontRuns() { + let smallFont = CTFontCreateWithName("Helvetica" as CFString, 12, nil) + let largeFont = CTFontCreateWithName("Helvetica" as CFString, 24, nil) + let expectedCapHeight = max(CTFontGetCapHeight(smallFont), CTFontGetCapHeight(largeFont)) + let expectedAscender = max(CTFontGetAscent(smallFont), CTFontGetAscent(largeFont)) + let expectedDescender = -max(CTFontGetDescent(smallFont), CTFontGetDescent(largeFont)) + let expectedLeading = max(CTFontGetLeading(smallFont), CTFontGetLeading(largeFont)) + + for (firstFont, secondFont) in [(smallFont, largeFont), (largeFont, smallFont)] { + let attributedString = NSMutableAttributedString(string: "ab") + attributedString.addAttribute(.kitFont, value: firstFont, range: NSRange(location: 0, length: 1)) + attributedString.addAttribute(.kitFont, value: secondFont, range: NSRange(location: 1, length: 1)) + + let metrics = attributedString.maxFontMetrics + + #expect(metrics.capHeight == expectedCapHeight) + #expect(metrics.ascender == expectedAscender) + #expect(metrics.descender == expectedDescender) + #expect(metrics.leading == expectedLeading) + } + } +} + +#endif From f5d40a683cef56dcfb099878e0234544501f455b Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 01:18:47 +0800 Subject: [PATCH 05/16] Update TextDrawingContext --- .../Shim/TextNonDarwinShims.swift | 8 ++++++++ .../View/Text/Text/Text+View.swift | 18 +++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift index 2eafb11fc..987a001b1 100644 --- a/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift @@ -8,6 +8,14 @@ public import UIFoundation_Private public typealias NSInteger = Int +class NSStringDrawingContext: NSObject { + var wrapsForTruncationMode = false + var wantsBaselineOffset = false + var wantsScaledLineHeight = false + var wantsScaledBaselineOffset = false + var cachesLayout = false +} + package enum NSWritingDirection: Int { case natural = -1 case leftToRight = 0 diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 8210c32dd..b874cf096 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -819,31 +819,23 @@ extension ResolvedStyledText { // 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 = { + private init() { 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) - }() + self.ctx = ctx + } + + static let shared = TextDrawingContext() } @_spi(ForOpenSwiftUIOnly) From fcaedaf657cf925b21cfe6411a237217b1b09e01 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 01:40:23 +0800 Subject: [PATCH 06/16] Update ResolvedStyledText.init --- .../View/Text/Text/Text+Sizing.swift | 55 ++++++++++++++++--- .../View/Text/Text/Text+View.swift | 47 ++++++++++++++-- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift index f478180e2..ce404ea67 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Sizing.swift @@ -6,6 +6,8 @@ // Status: Complete // ID: 22747AAF70EE5063D02F299CE90A18BE (SwiftUICore) +import Foundation + // MARK: TextSizingModifier protocol TextSizingModifier: Equatable { @@ -76,15 +78,50 @@ extension Text { } } -//extension Text.Sizing { -// func layoutMargins( -// for: NSAttributedString, -// metrics: inout NSAttributedString.EncodedFontMetrics? -// layoutProperties: LayoutProperties -// ) { -// _openSwiftUIUnimplementedFailure() -// } -//} +extension Text.Sizing { + func layoutMargins( + for attributedString: NSAttributedString, + metrics: inout NSAttributedString.EncodedFontMetrics?, + layoutProperties: TextLayoutProperties + ) -> EdgeInsets { + var margins: EdgeInsets + switch storage { + case .standard: + margins = .zero + case .uniformLineHeight: + let fontMetrics = attributedString.maxFontMetrics + metrics = fontMetrics + if let leading = metrics?.leading, leading != 0 { + let fontLineHeight = fontMetrics.ascender - fontMetrics.descender + let roundedLineHeight = fontLineHeight.rounded(.up, toMultipleOf: layoutProperties.pixelLength) + let adjustment = (leading + fontLineHeight - roundedLineHeight) * 0.5 + margins = EdgeInsets(horizontal: 0, vertical: adjustment) + } else { + margins = .zero + } + case .adjustsForOversizedCharacters: + #if canImport(CoreText) + margins = attributedString.oversizedDrawingMargin(from: NSAttributedString.oversizedScalarsWithoutEmoji) + margins.round(.up, toMultipleOf: layoutProperties.pixelLength) + #else + _openSwiftUIPlatformUnimplementedWarning() + margins = .zero + #endif + } + if layoutProperties.writingMode == .verticalRightToLeft { + margins = EdgeInsets( + top: margins.leading, + leading: margins.bottom, + bottom: margins.trailing, + trailing: margins.top + ) + } + for modifier in modifiers.reversed() { + modifier.updateLayoutMargins(&margins) + } + return margins + } +} private struct TextSizingKey: EnvironmentKey { static let defaultValue: Text.Sizing = .standard diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index b874cf096..a0cff4236 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -473,6 +473,31 @@ extension NSAttributedString { } return fonts } + + func oversizedDrawingMargin(from characterSet: CharacterSet) -> EdgeInsets { + guard string.rangeOfCharacter(from: characterSet) != nil else { + return .zero + } + return allFonts().reduce(into: .zero) { margins, font in + var left: CGFloat = 0 + var top: CGFloat = 0 + var right: CGFloat = 0 + var bottom: CGFloat = 0 + guard CTFontGetLanguageAwareOutsets( + font, + &left, + &top, + &right, + &bottom + ) else { + return + } + margins.top = max(margins.top, top) + margins.leading = max(margins.leading, left) + margins.bottom = max(margins.bottom, bottom) + margins.trailing = max(margins.trailing, right) + } + } } #endif @@ -568,23 +593,33 @@ package class ResolvedStyledText: CustomStringConvertible { archiveOptions: ArchivedViewInput.Value, isCollapsible: Bool, features: Text.ResolvedProperties.Features, - suffix: ResolvedTextSuffix, - attachments: Text.ResolvedProperties.CustomAttachments, + suffix _: ResolvedTextSuffix, + attachments _: Text.ResolvedProperties.CustomAttachments, styles: [_ShapeStyle_Pack.Style], transitions: [Text.ResolvedProperties.Transition], - scaleFactorOverride: CGFloat? + scaleFactorOverride _: CGFloat? ) { - // FIXME - _openSwiftUIUnimplementedWarning() self.storage = storage self.layoutProperties = layoutProperties - self.layoutMargins = layoutMargins ?? .zero self.stylePadding = stylePadding self.archiveOptions = archiveOptions self.isCollapsible = isCollapsible self.features = features self.styles = styles self.transitions = transitions + var computedMaxFontMetrics: NSAttributedString.EncodedFontMetrics? = nil + if let layoutMargins { + self.layoutMargins = layoutMargins + } else if let storage { + self.layoutMargins = layoutProperties.textSizing.layoutMargins( + for: storage, + metrics: &computedMaxFontMetrics, + layoutProperties: layoutProperties + ) + } else { + self.layoutMargins = .zero + } + self._computedMaxFontMetrics = computedMaxFontMetrics } package func lineHeightScalingAdjustment( From c2b64f0f7d0c72d160ba4f95cb70dda274639f02 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 02:04:35 +0800 Subject: [PATCH 07/16] Update ResolvedStyledText.lineHeightScalingAdjustment --- .../View/Text/Text/Text+View.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index a0cff4236..da92afd9f 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -627,7 +627,24 @@ package class ResolvedStyledText: CustomStringConvertible { maximumLineHeight: CGFloat, minimumLineHeight: CGFloat ) -> CGFloat { - _openSwiftUIUnimplementedFailure() + guard (lineHeightMultiple != 0 && lineHeightMultiple < 1) || + maximumLineHeight != 0 || + minimumLineHeight != 0 + else { + return .zero + } + let fontMetrics = maxFontMetrics + let fontLineHeight = fontMetrics.ascender - fontMetrics.descender + let multiple = lineHeightMultiple == 0 ? 1 : lineHeightMultiple + let proposedLineHeight = multiple * fontLineHeight + let maximum = maximumLineHeight == 0 ? proposedLineHeight : maximumLineHeight + let constrainedLineHeight = proposedLineHeight.clamp( + min: minimumLineHeight, + max: maximum + ) + return constrainedLineHeight < fontLineHeight + ? fontLineHeight - constrainedLineHeight + : .zero } final package func draw( From 6f94cc79e5f66db2a3f60e010ec017bec1231e98 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 02:10:25 +0800 Subject: [PATCH 08/16] Update ResolvedStyledText abstract method --- .../View/Text/Text/Text+View.swift | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index da92afd9f..f712cb939 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -654,37 +654,41 @@ package class ResolvedStyledText: CustomStringConvertible { context: TextDrawingContext = .shared, renderer: TextRendererBoxBase? = nil ) { - _openSwiftUIUnimplementedFailure() + draw( + in: drawingArea, + with: measuredSize, + applyingMarginOffsets: applyingMarginOffsets, + containsResolvable: isDynamic, + context: context, + renderer: renderer + ) } package var majorAxis: Axis { - _openSwiftUIUnimplementedFailure() + _openSwiftUIBaseClassAbstractMethod() } package func drawingScale(size: CGSize) -> CGFloat { - _openSwiftUIUnimplementedFailure() + _openSwiftUIBaseClassAbstractMethod() } package func spacing() -> Spacing { - _openSwiftUIUnimplementedWarning() - return .init() + _openSwiftUIBaseClassAbstractMethod() } package func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize { - _openSwiftUIUnimplementedWarning() - return CGSize(width: 50, height: 50) + _openSwiftUIBaseClassAbstractMethod() } package func size(in request: CGSize) -> CGSize { - _openSwiftUIUnimplementedWarning() - return CGSize(width: 50, height: 50) + _openSwiftUIBaseClassAbstractMethod() } package func frameSize(in request: CGSize) -> CGSize { - _openSwiftUIUnimplementedWarning() - return CGSize(width: 50, height: 50) + size(in: request) } + // FIXME package func _deleteMethod1() { _openSwiftUIUnimplementedFailure() } @@ -697,22 +701,21 @@ package class ResolvedStyledText: CustomStringConvertible { in request: CGSize, context: TextDrawingContext ) -> CGSize { - _openSwiftUIUnimplementedFailure() + _openSwiftUIBaseClassAbstractMethod() } package func explicitAlignment( _ k: AlignmentKey, at size: CGSize ) -> CGFloat? { - _openSwiftUIUnimplementedWarning() - return nil + _openSwiftUIBaseClassAbstractMethod() } package func linkURL( at point: CGPoint, in size: CGSize ) -> URL? { - _openSwiftUIUnimplementedFailure() + _openSwiftUIBaseClassAbstractMethod() } package func draw( @@ -723,14 +726,7 @@ package class ResolvedStyledText: CustomStringConvertible { context: TextDrawingContext, renderer: TextRendererBoxBase? = nil ) { - #if canImport(Darwin) - var r = drawingArea - r.y = r.height - self.storage?.draw(with: r, context: context.ctx) - _openSwiftUIUnimplementedWarning() - #else - _openSwiftUIPlatformUnimplementedFailure() - #endif + _openSwiftUIBaseClassAbstractMethod() } package func layoutValue( @@ -738,9 +734,11 @@ package class ResolvedStyledText: CustomStringConvertible { with measuredSize: CGSize, applyingMarginOffsets: Bool = true ) -> Text.Layout? { - _openSwiftUIUnimplementedFailure() + nil } + // TBA + final package func resolvedContent(in context: ResolvableStringResolutionContext) -> NSAttributedString? { _openSwiftUIUnimplementedFailure() } From 427a3cf5d651c27b5ec6c037f4e7327652992d13 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 02:21:49 +0800 Subject: [PATCH 09/16] Add NSAttributedString.Metrics API --- .../View/Text/Text/Text+Measure.swift | 72 +++++++++++++++++++ .../View/Text/Text/Text+View.swift | 12 ++-- 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift new file mode 100644 index 000000000..cd89096be --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift @@ -0,0 +1,72 @@ +// +// Text+StringDrawingContext.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 68D51D45BC7225E72FB030F887E5A492 (SwiftUICore) + +public import Foundation + +@available(OpenSwiftUI_v6_0, *) +extension NSAttributedString { + @usableFromInline + package struct Metrics: Equatable { + package var size: CGSize + package let scale: CGFloat + package var firstBaseline: CGFloat + package var lastBaseline: CGFloat + package var baselineAdjustment: CGFloat + package var requestedWidth: CGFloat + package var numberOfLines: UInt? + package var hasTruncatedRanges: Bool + + package init( + size: CGSize, + scale: CGFloat, + firstBaseline: CGFloat, + lastBaseline: CGFloat, + baselineAdjustment: CGFloat, + requestedWidth: CGFloat, + numberOfLines: UInt?, + hasTruncatedRanges: Bool + ) { + self.size = size + self.scale = scale + self.firstBaseline = firstBaseline + self.lastBaseline = lastBaseline + self.baselineAdjustment = baselineAdjustment + self.requestedWidth = requestedWidth + self.numberOfLines = numberOfLines + self.hasTruncatedRanges = hasTruncatedRanges + } + } + + // TODO + struct CacheMetrics {} + + private static let emptyString = NSAttributedString() + + private func measured( + requestedSize: CGSize, + lineLimit: Int?, + lowerLineLimit: Int?, + minScaleFactor: CGFloat, + bodyHeadOutdent: CGFloat, + widthIsFlexible: Bool, + kitCache: inout AnyObject?, + isCollapsible: Bool, + wantsNumberOfLineFragments: Bool, + context: TextDrawingContext + ) -> NSAttributedString.Metrics { + _openSwiftUIUnimplementedFailure() + } +} + +@available(*, unavailable) +extension NSAttributedString.Metrics: Sendable {} + +extension ResolvedStyledText { + // FIXME + package class StringDrawing: ResolvedStyledText {} +} diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index f712cb939..376586273 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -693,8 +693,11 @@ package class ResolvedStyledText: CustomStringConvertible { _openSwiftUIUnimplementedFailure() } - package func _deleteMethod2() { - _openSwiftUIUnimplementedFailure() + package func metrics( + in size: CGSize, + layoutMargins: EdgeInsets? + ) -> NSAttributedString.Metrics { + _openSwiftUIBaseClassAbstractMethod() } package func size( @@ -862,11 +865,6 @@ extension ResolvedStyledText { } } -extension ResolvedStyledText { - // FIXME - package class StringDrawing: ResolvedStyledText {} -} - // MARK: - TextDrawingContext @_spi(ForOpenSwiftUIOnly) From 3cfbe4091b99fda52871c071b767c90efc5f5659 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 02:45:20 +0800 Subject: [PATCH 10/16] Update ResolvedStyledText missing API --- .../View/Text/Text/Text+View.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 376586273..d6531f7fe 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -501,7 +501,7 @@ extension NSAttributedString { } #endif -// MARK: - ResolvedStyledText [WIP] +// MARK: - ResolvedStyledText [Blocked by ORB] @available(OpenSwiftUI_v6_0, *) @usableFromInline @@ -767,21 +767,26 @@ package class ResolvedStyledText: CustomStringConvertible { } final package var updatesAsynchronously: Bool { - _openSwiftUIUnimplementedFailure() + isDynamic && archiveOptions.isArchived } @usableFromInline final package var description: String { - _openSwiftUIUnimplementedFailure() + storage?.string ?? "" } final package var accessibilityText: Text { - _openSwiftUIUnimplementedFailure() + guard let storage else { + return Text("") + } + return AccessibilityText(storage).text } -// final package var cgStyleHandler: RBCGStyleHandler? { -// _openSwiftUIUnimplementedFailure() -// } + // TODO: RB + + final package var cgStyleHandler: ORBCGStyleHandler? { + _openSwiftUIUnimplementedFailure() + } final package func makeRBDisplayList( for size: CGSize, @@ -792,6 +797,9 @@ package class ResolvedStyledText: CustomStringConvertible { } } +// FIXME: RB +package class ORBCGStyleHandler {} + extension ResolvedStyledText { package func textSizeCacheMetrics(in size: CGSize) -> (UInt?, CGSize) { _openSwiftUIUnimplementedFailure() From 05e1ddcd4a94b4d538c15344930e2b3f7042468e Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 15:07:57 +0800 Subject: [PATCH 11/16] Update metrics and layout API --- .../OpenSwiftUICore/View/Text/Text/Text+View.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index d6531f7fe..17122ee66 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -797,25 +797,26 @@ package class ResolvedStyledText: CustomStringConvertible { } } -// FIXME: RB -package class ORBCGStyleHandler {} - extension ResolvedStyledText { package func textSizeCacheMetrics(in size: CGSize) -> (UInt?, CGSize) { - _openSwiftUIUnimplementedFailure() + let metrics = metrics(in: size, layoutMargins: nil) + return (metrics.numberOfLines, metrics.size) } package func linkURLMetrics( in size: CGSize, layoutMargins: EdgeInsets ) -> CGFloat { - _openSwiftUIUnimplementedFailure() + metrics(in: size, layoutMargins: layoutMargins).scale } } @available(*, unavailable) extension ResolvedStyledText: Sendable {} +// FIXME: RB +package class ORBCGStyleHandler {} + // MARK: - ResolvedStyledText + layout extension extension ResolvedStyledText { @@ -830,7 +831,7 @@ extension ResolvedStyledText { explicitAlignment( VerticalAlignment.lastTextBaseline.key, at: size - ) ?? .zero + ) ?? size.height } package func frame(in request: CGSize, renderer: TextRendererBoxBase?) -> CGRect { From 2573597a57a11aca32e64ba606d0d979b6111469 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 20 Jul 2026 02:51:00 +0800 Subject: [PATCH 12/16] [WIP] Update resolving API --- .../View/Text/Text/Text+View.swift | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 17122ee66..124ea774f 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -740,14 +740,42 @@ package class ResolvedStyledText: CustomStringConvertible { nil } - // TBA - final package func resolvedContent(in context: ResolvableStringResolutionContext) -> NSAttributedString? { - _openSwiftUIUnimplementedFailure() + guard let storage else { + return nil + } + guard storage.isDynamic, archiveOptions.isArchived else { + return storage + } + guard let mutable = storage.mutableCopy() as? NSMutableAttributedString else { + return nil + } + ResolvableTextSegmentAttribute.update(mutable, in: context) + return mutable } final package func resolvingContent(in context: ResolvableStringResolutionContext) -> ResolvedStyledText { - _openSwiftUIUnimplementedFailure() + guard let storage, + storage.isDynamic, + let mutable = storage.mutableCopy() as? NSMutableAttributedString + else { + return self + } + ResolvableTextSegmentAttribute.update(mutable, in: context) + return ResolvedStyledText.styledText( + storage: mutable, + layoutProperties: layoutProperties, + layoutMargins: nil, + stylePadding: stylePadding, + archiveOptions: archiveOptions, + isCollapsible: isCollapsible, + features: features, + suffix: .none, + attachments: .init(), + styles: styles, + transitions: transitions, + scaleFactorOverride: scaleFactorOverride + ) } final package func nextUpdate( @@ -901,51 +929,59 @@ 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 -//} +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 { + _openSwiftUIUnimplementedFailure() + } + + 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 { + _openSwiftUIUnimplementedFailure() + } + + + 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 { + _openSwiftUIUnimplementedFailure() + } + +} // MARK: - CodableResolvedStyledText [WIP] From 12343355cd63072250583a1ee4355dfdac7a7a7e Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 21 Jul 2026 01:05:26 +0800 Subject: [PATCH 13/16] [NFC] Optimize AttributeCountTestInfo --- .../Test/AttributeCountInfoKey.swift | 19 +++++++++++++++++++ .../Test/AttributeCountTestInfo.swift | 17 ++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Test/AttributeCountInfoKey.swift diff --git a/Sources/OpenSwiftUICore/Test/AttributeCountInfoKey.swift b/Sources/OpenSwiftUICore/Test/AttributeCountInfoKey.swift new file mode 100644 index 000000000..4ce3cd6a7 --- /dev/null +++ b/Sources/OpenSwiftUICore/Test/AttributeCountInfoKey.swift @@ -0,0 +1,19 @@ +// +// AttributeCountInfoKey.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - AttributeCountInfoKey + +package struct AttributeCountInfoKey: HostPreferenceKey { + package static let defaultValue = AttributeCountTestInfo() + + package static func reduce( + value: inout AttributeCountTestInfo, + nextValue: () -> AttributeCountTestInfo + ) { + value.merge(nextValue()) + } +} diff --git a/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift b/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift index 354704fb9..990bcd7d8 100644 --- a/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift +++ b/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift @@ -2,9 +2,10 @@ // AttributeCountTestInfo.swift // OpenSwiftUICore // +// Audited for 6.5.4 // Status: Complete -// MARK: - AttributeCountTestInfo [6.4.41] +// MARK: - AttributeCountTestInfo package struct AttributeCountTestInfo: Equatable { var attributeCounts: [String: UInt32] = [:] @@ -19,17 +20,3 @@ package struct AttributeCountTestInfo: Equatable { history.merge(other.history) { $0 + $1 } } } - -// MARK: - AttributeCountInfoKey [6.4.41] - -package struct AttributeCountInfoKey: HostPreferenceKey { - package static let defaultValue = AttributeCountTestInfo() - - package static func reduce( - value: inout AttributeCountTestInfo, - nextValue: () -> AttributeCountTestInfo - ) { - value.merge(nextValue()) - } -} - From 92cb7492981750e87039ca956a4f989270a62e09 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 21 Jul 2026 01:32:15 +0800 Subject: [PATCH 14/16] Update ResolvedStyledText + styledText --- .../View/Text/Text/Text+Measure.swift | 3 + .../View/Text/Text/Text+View.swift | 90 ++++++++++++++++--- 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift index cd89096be..720e9a650 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift @@ -69,4 +69,7 @@ extension NSAttributedString.Metrics: Sendable {} extension ResolvedStyledText { // FIXME package class StringDrawing: ResolvedStyledText {} + + // FIXME + package class TextLayoutManager: ResolvedStyledText {} } diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 124ea774f..3f95b68c8 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -312,7 +312,7 @@ public struct TextLayoutProperties: Equatable { package var textSizing: Text.Sizing = .standard - private var textShape: TextShape = .bounds + fileprivate var textShape: TextShape = .bounds private struct Flags: OptionSet { var rawValue: UInt8 @@ -927,7 +927,7 @@ final public class TextDrawingContext { @available(*, unavailable) extension TextDrawingContext: Sendable {} -// MARK: - ResolvedStyledText + Extension [WIP] +// MARK: - ResolvedStyledText + styledText extension ResolvedStyledText { package static func styledText( @@ -942,10 +942,51 @@ extension ResolvedStyledText { attachments: Text.ResolvedProperties.CustomAttachments, styles: [_ShapeStyle_Pack.Style], transitions: [Text.ResolvedProperties.Transition], - scaleFactorOverride: CGFloat?, - isInitialResolution: Bool = true + scaleFactorOverride: CGFloat? ) -> ResolvedStyledText { - _openSwiftUIUnimplementedFailure() + let layoutManagerFeatures: Text.ResolvedProperties.Features = [ + .customRenderer, + .useTextLayoutManager, + .produceTextLayout, + .checkInterpolationStrategy, + ] + let requiresTextLayoutManager = + layoutProperties.writingMode != .horizontalTopToBottom || + !features.intersection(layoutManagerFeatures).isEmpty || + !attachments.isEmpty || + suffix != .none || + layoutProperties.textShape != .bounds + if requiresTextLayoutManager { + return TextLayoutManager( + storage: storage, + layoutProperties: layoutProperties, + layoutMargins: layoutMargins, + stylePadding: stylePadding, + archiveOptions: archiveOptions, + isCollapsible: isCollapsible, + features: features, + suffix: suffix, + attachments: attachments, + styles: styles, + transitions: transitions, + scaleFactorOverride: scaleFactorOverride + ) + } else { + return StringDrawing( + storage: storage, + layoutProperties: layoutProperties, + layoutMargins: layoutMargins, + stylePadding: stylePadding, + archiveOptions: archiveOptions, + isCollapsible: isCollapsible, + features: features, + suffix: suffix, + attachments: attachments, + styles: styles, + transitions: transitions, + scaleFactorOverride: scaleFactorOverride + ) + } } package static func styledText( @@ -960,10 +1001,22 @@ extension ResolvedStyledText { styles: [_ShapeStyle_Pack.Style] = .init(), transitions: [Text.ResolvedProperties.Transition] = .init() ) -> ResolvedStyledText { - _openSwiftUIUnimplementedFailure() + styledText( + storage: storage, + layoutProperties: layoutProperties, + layoutMargins: nil, + stylePadding: stylePadding, + archiveOptions: archiveOptions, + isCollapsible: isCollapsible, + features: features, + suffix: suffix, + attachments: attachments, + styles: styles, + transitions: transitions, + scaleFactorOverride: nil + ) } - package static func styledText( storage: NSAttributedString?, stylePadding: EdgeInsets = EdgeInsets(), @@ -975,12 +1028,29 @@ extension ResolvedStyledText { attachments: Text.ResolvedProperties.CustomAttachments = .init(), styles: [_ShapeStyle_Pack.Style] = .init(), transitions: [Text.ResolvedProperties.Transition] = .init(), - writingMode: Text.WritingMode? = nil, + writingMode: Text.WritingMode?, sizeFitting: Bool = false ) -> ResolvedStyledText { - _openSwiftUIUnimplementedFailure() + var layoutProperties = TextLayoutProperties(environment) + if let writingMode { + layoutProperties.writingMode = writingMode + } + layoutProperties.sizeFitting = sizeFitting + return styledText( + storage: storage, + layoutProperties: layoutProperties, + layoutMargins: nil, + stylePadding: stylePadding, + archiveOptions: archiveOptions, + isCollapsible: isCollapsible, + features: features, + suffix: suffix, + attachments: attachments, + styles: styles, + transitions: transitions, + scaleFactorOverride: nil + ) } - } // MARK: - CodableResolvedStyledText [WIP] From a0211d8623d584e2efb3f301e88f3c83047ba832 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 21 Jul 2026 01:56:23 +0800 Subject: [PATCH 15/16] Add MetricsCache --- .../View/Text/Text/Text+Measure.swift | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift index 720e9a650..afd08915a 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+Measure.swift @@ -42,8 +42,55 @@ extension NSAttributedString { } } - // TODO - struct CacheMetrics {} + struct MetricsCache { + var kitCache: AnyObject? + let string: NSAttributedString + let lineLimit: Int? + let lowerLineLimit: Int? + let minScaleFactor: CGFloat + let bodyHeadOutdent: CGFloat + let pixelLength: CGFloat + let widthIsFlexible: Bool + let drawWithRequestedWidth: Bool + let isCollapsible: Bool + var entries: [(CGSize, Metrics)] + + init( + _ string: NSAttributedString?, + scaleFactorOverride: CGFloat?, + lineLimit: Int?, + lowerLineLimit: Int?, + minScaleFactor: CGFloat, + bodyHeadOutdent: CGFloat, + pixelLength: CGFloat, + widthIsFlexible: Bool, + drawWithRequestedWidth: Bool, + isCollapsible: Bool + ) { + self.kitCache = nil + self.string = string?.scaled(by: scaleFactorOverride ?? 1) ?? .emptyString + self.lineLimit = lineLimit + self.lowerLineLimit = lowerLineLimit + // A scale override has already been baked into `string`, so the drawing + // context must not apply the original minimum scale factor a second time. + self.minScaleFactor = scaleFactorOverride == nil ? minScaleFactor : 1 + self.bodyHeadOutdent = bodyHeadOutdent + self.pixelLength = pixelLength + self.widthIsFlexible = widthIsFlexible + self.drawWithRequestedWidth = drawWithRequestedWidth + self.isCollapsible = isCollapsible + self.entries = [] + } + + mutating func metrics( + requestedSize: CGSize, + layoutMargins: EdgeInsets, + wantsNumberOfLineFragments: Bool, + context: TextDrawingContext + ) -> Metrics { + _openSwiftUIUnimplementedFailure() + } + } private static let emptyString = NSAttributedString() From 666b529501b93ec92ab5fcda5ddb41c5824c9441 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 21 Jul 2026 03:06:27 +0800 Subject: [PATCH 16/16] Workaround Linux compile issue --- Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift b/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift index 987a001b1..343683582 100644 --- a/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift +++ b/Sources/OpenSwiftUICore/Shim/TextNonDarwinShims.swift @@ -91,6 +91,12 @@ package class NSTextLineFragment: NSObject { } extension NSAttributedString { + // Work around https://github.com/swiftlang/swift/issues/71874. + @_nonoverride + public convenience init() { + self.init(string: "") + } + public convenience init(_ attributedString: AttributedString) { self.init(string: String(attributedString)) }