-
Notifications
You must be signed in to change notification settings - Fork 119
Expand file tree
/
Copy pathTextFragment.swift
More file actions
85 lines (74 loc) · 2.98 KB
/
Copy pathTextFragment.swift
File metadata and controls
85 lines (74 loc) · 2.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import SwiftUI
// MARK: - Overview
//
// TextFragment renders attributed content as SwiftUI.Text with support for inline
// attachments, links, and selection. It uses a TextBuilder to construct and cache
// Text values, minimizing rebuilds during resize by keying on attachment sizes.
//
// Attachments are represented as placeholder images tagged with AttachmentAttribute. The
// actual attachment views are rendered in an overlay using the resolved Text.Layout
// geometry. Three modifiers are applied at the fragment level:
//
// - TextSelectionBackground renders selection highlights on macOS
// - AttachmentOverlay draws attachments at their run locations with selection-aware dimming
// - TextLinkInteraction handles tap gestures on links
//
// These overlays use backgroundPreferenceValue and overlayPreferenceValue to access
// Text.Layout and render in fragment-local coordinates. Fragment-level overlays enable
// coordinate space isolation and keep scrollable regions interactive.
//
// An ancestor view must define a named coordinate space (.textContainer) for the text
// container. TextFragment uses onGeometryChange to observe the container size and rebuild
// Text when attachment sizes need to change.
//
// TextFragment is used by InlineText and StructuredText (via BlockContent) to render
// attributed content with inline attachments, links, and selection.
struct TextFragment<Content: AttributedStringProtocol>: View {
@Environment(\.textEnvironment) private var textEnvironment
@State private var textBuilder: TextBuilder?
private let content: Content
init(_ content: Content) {
self.content = content
}
var body: some View {
taggedText
.onGeometryChange(for: CGSize?.self, of: \.textContainerSize) { size in
guard let size, let textBuilder else { return }
textBuilder.sizeChanged(size, environment: textEnvironment)
}
.onChange(of: content, initial: true) { _, newValue in
self.textBuilder = TextBuilder(newValue, environment: textEnvironment)
}
.modifier(TextSelectionBackground())
.modifier(AttachmentOverlay(attachments: content.attachments()))
.modifier(TextLinkInteraction())
}
@ViewBuilder private var taggedText: some View {
if #available(iOS 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) {
text.customAttribute(TextFragmentAttribute())
} else {
text
}
}
private var text: Text {
textBuilder?.text ?? Text(verbatim: "")
}
}
@available(iOS 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *)
struct TextFragmentAttribute: TextAttribute {}
@available(iOS 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *)
extension Text.Layout {
var isTextFragment: Bool {
first?.first?[TextFragmentAttribute.self] != nil
}
}
extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace {
static var textContainer: NamedCoordinateSpace {
.named("textContainer")
}
}
extension GeometryProxy {
fileprivate var textContainerSize: CGSize? {
bounds(of: .textContainer)?.size
}
}