File: PlatformIntelligenceTextEffectView.swift

package info (click to toggle)
webkit2gtk 2.48.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 429,764 kB
  • sloc: cpp: 3,697,587; javascript: 194,444; ansic: 169,997; python: 46,499; asm: 19,295; ruby: 18,528; perl: 16,602; xml: 4,650; yacc: 2,360; sh: 2,098; java: 1,993; lex: 1,327; pascal: 366; makefile: 298
file content (643 lines) | stat: -rw-r--r-- 27,622 bytes parent folder | download | duplicates (6)
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
// Copyright (C) 2024-2025 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.

import Foundation

#if canImport(WritingTools)

#if canImport(AppKit)
import AppKit
// WritingToolsUI is not present in the base system, but WebKit is, so it must be weak-linked.
// WritingToolsUI need not be soft-linked from WebKitSwift because although WTUI links WebKit, WebKit does not directly link WebKitSwift.
@_weakLinked internal import WritingToolsUI_Private._WTTextEffectView
@_weakLinked internal import WritingToolsUI_Private._WTSweepTextEffect
@_weakLinked internal import WritingToolsUI_Private._WTReplaceTextEffect
#else
internal import UIKit_Private
@_spi(TextEffects) import UIKit
#endif

import WebKitSwift
// Work around rdar://145157171 by manually importing the cross-import module.
#if canImport(_WebKit_SwiftUI)
internal import _WebKit_SwiftUI
#endif
internal import SwiftUI

// MARK: Platform abstraction type aliases

#if canImport(AppKit)
typealias PlatformView = NSView
typealias PlatformBounds = NSRect
typealias PlatformTextPreview = [_WTTextPreview]
#else
typealias PlatformView = UIView
typealias PlatformBounds = CGRect
typealias PlatformTextPreview = UITargetedPreview
#endif

struct PlatformContentPreview {
    let previewImage: CGImage?
    let presentationFrame: CGRect
}

// MARK: Platform abstraction protocols

/// Some arbitrary data which can be translated to and represented by a text preview,
/// and also be able to be identified.
protocol PlatformIntelligenceTextEffectChunk: Identifiable {
}

/// Either a pondering or replacement effect.
@MainActor protocol PlatformIntelligenceTextEffect<Chunk>: Equatable, Identifiable where ID == PlatformIntelligenceTextEffectID {
    associatedtype Chunk: PlatformIntelligenceTextEffectChunk

    var chunk: Chunk { get }

    // Clients should not invoke this function directly.
    func _add<Source>(to view: PlatformIntelligenceTextEffectView<Source>) async where Source: PlatformIntelligenceTextEffectViewSource, Source.Chunk == Chunk
}

extension PlatformIntelligenceTextEffect {
    nonisolated static func ==(lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id
    }
}

/// A combination source+delegate protocol that clients conform to to control behavior and yield information to the effect view.
@MainActor protocol PlatformIntelligenceTextEffectViewSource: AnyObject {
    associatedtype Chunk: PlatformIntelligenceTextEffectChunk

    /// Transforms an arbitrary chunk into a text preview.
    func textPreview(for chunk: Chunk) async -> PlatformTextPreview?

    /// Controls the visibility of text associated with the specified chunk.
    func updateTextChunkVisibility(_ chunk: Chunk, visible: Bool) async

    /// In the implementation of this method, clients should replace the backing text storage (which mustn't be visible to the user).
    /// Then, a preview of the resulting text should be created.
    ///
    /// Clients must also take the responsibility of animating the remaining text away from the replaced text if needed, using
    /// the provided animation parameters.
    func performReplacementAndGeneratePreview(for chunk: Chunk, effect: PlatformIntelligenceReplacementTextEffect<Chunk>) async -> (PlatformTextPreview?, remainder: PlatformContentPreview?)

    /// This function is invoked after preparing the replacement effect, but before the effect is added.
    func replacementEffectWillBegin(_ effect: PlatformIntelligenceReplacementTextEffect<Chunk>) async

    /// This function is invoked once both parts of the replacement effect are complete.
    func replacementEffectDidComplete(_ effect: PlatformIntelligenceReplacementTextEffect<Chunk>) async
}

// MARK: Platform type adapters.

#if canImport(UIKit)

@MainActor private final class UITextEffectViewSourceAdapter<Wrapped>: NSObject, UITextEffectViewSource where Wrapped: PlatformIntelligenceTextEffectViewSource {
    private var wrapped: Wrapped

    init(wrapping wrapped: Wrapped) {
        self.wrapped = wrapped
    }

    // This method needs to be `@objc`, and the type itself needs to conform to `NSObject`, otherwise this method will always return `true`.
    //
    // This is because internally, UIKit creates a type with a default conformance to this protocol, and a default implementation of this method.
    // The default implementation ostensibly requires the real conforming type to be an `NSObject`, and if not will return `true`. And then, if
    // it is an `NSObject`, it performs a selector check, which requires an `@objc` implementation, else it will fail and once again return `true`.
    @objc func canGenerateTargetedPreviewForChunk(_ chunk: UITextEffectTextChunk) async -> Bool {
        if let chunk = chunk as? UIPonderingTextEffectTextChunkAdapter<Wrapped.Chunk> {
            return true
        }

        if let chunk = chunk as? UIReplacementTextEffectTextChunkAdapter<Wrapped.Chunk> {
            return chunk.source != nil
        }

        return false
    }

    func targetedPreview(for chunk: UITextEffectTextChunk) async -> UITargetedPreview {
        if let chunk = chunk as? UIPonderingTextEffectTextChunkAdapter<Wrapped.Chunk> {
            return chunk.preview
        }

        if let chunk = chunk as? UIReplacementTextEffectTextChunkAdapter<Wrapped.Chunk> {
            // The chunk source may be `nil` in the case of a replacement whose source range is an empty range.
            // This force unwrap is safe because UIKit invokes `canGenerateTargetedPreviewForChunk` prior to this call.
            return chunk.source!
        }

        fatalError("Failed to create a targeted preview: parameter was of unexpected type \(type(of: chunk)).")
    }

    func updateTextChunkVisibilityForAnimation(_ chunk: UITextEffectTextChunk, visible: Bool) async {
        if let chunk = chunk as? UIPonderingTextEffectTextChunkAdapter<Wrapped.Chunk> {
            await self.wrapped.updateTextChunkVisibility(chunk.wrapped, visible: visible)
        }

        if let chunk = chunk as? UIReplacementTextEffectTextChunkAdapter<Wrapped.Chunk> {
            await self.wrapped.updateTextChunkVisibility(chunk.wrapped, visible: visible)
        }
    }
}

@MainActor private final class UIReplacementTextEffectDelegateAdapter<Wrapped>: UITextEffectView.ReplacementTextEffect.Delegate where Wrapped: PlatformIntelligenceTextEffectViewSource {
    private let wrapped: Wrapped
    private weak var view: PlatformIntelligenceTextEffectView<Wrapped>?

    init(wrapping wrapped: Wrapped, view: PlatformIntelligenceTextEffectView<Wrapped>) {
        self.wrapped = wrapped
        self.view = view
    }

    func replacementEffectDidComplete(_ effect: UITextEffectView.ReplacementTextEffect) {
        guard let view = self.view else {
            assertionFailure("Failed to handle completion of replacement effect: view was unexpectedly nil.")
            return
        }

        guard let effect = view.wrappedEffectIDToPlatformEffects[effect.id] as? PlatformIntelligenceReplacementTextEffect<Wrapped.Chunk> else {
            assertionFailure("Failed to handle completion of replacement effect: effect was unexpectedly nil.")
            return
        }

        Task { @MainActor in
            await self.wrapped.replacementEffectDidComplete(effect)
        }
    }

    func performReplacementAndGeneratePreview(for chunk: UITextEffectTextChunk, effect: UITextEffectView.ReplacementTextEffect, animation: UITextEffectView.ReplacementTextEffect.AnimationParameters) async -> UITargetedPreview? {
        guard let chunk = chunk as? UIReplacementTextEffectTextChunkAdapter<Wrapped.Chunk> else {
            fatalError("Failed to perform replacement and generate preview: parameter was of unexpected type \(type(of: chunk)).")
        }

        return chunk.destination
    }
}

private final class UIPonderingTextEffectTextChunkAdapter<Wrapped>: UITextEffectTextChunk where Wrapped: PlatformIntelligenceTextEffectChunk {
    let wrapped: Wrapped
    let preview: UITargetedPreview

    init(wrapping wrapped: Wrapped, preview: UITargetedPreview) {
        self.wrapped = wrapped
        self.preview = preview
    }
}

private final class UIReplacementTextEffectTextChunkAdapter<Wrapped>: UITextEffectTextChunk where Wrapped: PlatformIntelligenceTextEffectChunk {
    let wrapped: Wrapped
    let source: UITargetedPreview?
    let destination: UITargetedPreview

    init(wrapping wrapped: Wrapped, source: UITargetedPreview?, destination: UITargetedPreview) {
        self.wrapped = wrapped
        self.source = source
        self.destination = destination
    }
}

#else

@MainActor private final class WTTextPreviewAsyncSourceAdapter<Wrapped>: NSObject, _WTTextPreviewAsyncSource where Wrapped: PlatformIntelligenceTextEffectViewSource {
    private let wrapped: Wrapped

    init(wrapping wrapped: Wrapped) {
        self.wrapped = wrapped
    }

    func textPreviews(for chunk: _WTTextChunk) async -> [_WTTextPreview]? {
        guard let chunk = chunk as? WTTextChunkAdapter<Wrapped.Chunk> else {
            fatalError("Failed to update text chunk visibility: parameter was of unexpected type \(type(of: chunk)).")
        }

        return chunk.preview
    }
    
    func textPreview(for rect: CGRect) async -> _WTTextPreview? {
        // This is implemented manually by the system instead of relying on the WTUI interface.
        nil
    }

    func updateIsTextVisible(_ isTextVisible: Bool, for chunk: _WTTextChunk) async {
        guard let chunk = chunk as? WTTextChunkAdapter<Wrapped.Chunk> else {
            fatalError("Failed to update text chunk visibility: parameter was of unexpected type \(type(of: chunk)).")
        }

        await self.wrapped.updateTextChunkVisibility(chunk.wrapped, visible: isTextVisible)
    }
}

private final class WTTextChunkAdapter<Wrapped>: _WTTextChunk where Wrapped: PlatformIntelligenceTextEffectChunk {
    let wrapped: Wrapped
    let preview: PlatformTextPreview?

    init(wrapping wrapped: Wrapped, preview: PlatformTextPreview?) {
        self.wrapped = wrapped
        self.preview = preview

        super.init(chunkWithIdentifier: UUID().uuidString)
    }
}

#endif

// MARK: Platform abstraction types

/// An opaque identifier for effects.
struct PlatformIntelligenceTextEffectID: Hashable {
    private let id = UUID()

    fileprivate init() {
    }
}

/// A platform-agnostic view to control intelligence text effects given a particular source.
@MainActor final class PlatformIntelligenceTextEffectView<Source>: PlatformView where Source: PlatformIntelligenceTextEffectViewSource {
#if canImport(UIKit)
    fileprivate typealias SourceAdapter = UITextEffectViewSourceAdapter<Source>
    fileprivate typealias Wrapped = UITextEffectView
#else
    fileprivate typealias SourceAdapter = WTTextPreviewAsyncSourceAdapter<Source>
    fileprivate typealias Wrapped = _WTTextEffectView
#endif

    fileprivate let source: Source
    fileprivate let wrapped: Wrapped

    private let viewSource: SourceAdapter

#if canImport(UIKit)
    fileprivate var wrappedEffectIDToPlatformEffects: [UITextEffectView.EffectID : any PlatformIntelligenceTextEffect] = [:]
    fileprivate var platformEffectIDToWrappedEffectIDs: [PlatformIntelligenceTextEffectID : UITextEffectView.EffectID] = [:]
#else
    fileprivate var wrappedEffectIDToPlatformEffects: [UUID : any PlatformIntelligenceTextEffect<Source.Chunk>] = [:]
    fileprivate var platformEffectIDToWrappedEffectIDs: [PlatformIntelligenceTextEffectID : Set<UUID>] = [:]
#endif

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /// Create a new text effect view.
    init(source: Source) {
        self.source = source
        self.viewSource = SourceAdapter(wrapping: self.source)

#if canImport(UIKit)
        self.wrapped = Wrapped(source: self.viewSource)
#else
        self.wrapped = Wrapped(asyncSource: self.viewSource)
#endif

        self.wrapped.clipsToBounds = true

        super.init(frame: .zero)
    }

    func initializeSubviews() {
        self.addSubview(self.wrapped)
        self.wrapped.frame = self.bounds
    }

    /// Prepares and adds an effect to be presented within the view.
    @discardableResult func addEffect<Effect>(_ effect: Effect) async -> Effect.ID where Effect: PlatformIntelligenceTextEffect, Effect.Chunk == Source.Chunk {
        await effect._add(to: self)
        return effect.id
    }

    /// Removes the effect with the specified id.
    func removeEffect(_ effectID: PlatformIntelligenceTextEffectID) async {
        guard let wrappedEffectIDs = self.platformEffectIDToWrappedEffectIDs.removeValue(forKey: effectID) else {
            return
        }

#if canImport(UIKit)
        self.wrappedEffectIDToPlatformEffects[wrappedEffectIDs] = nil
        self.wrapped.removeEffect(wrappedEffectIDs)
#else
        for wrappedEffectID in wrappedEffectIDs {
            if let platformEffect = self.wrappedEffectIDToPlatformEffects.removeValue(forKey: wrappedEffectID), platformEffect is PlatformIntelligencePonderingTextEffect<Source.Chunk> {
                // When WTUI starts a pondering effect, it creates a 0.75s opacity CA animation to fade out the text, so it is possible
                // that this is still ongoing by the time `removeEffect` is called. This may lead to issues if subsequent effects start
                // immediately after the effect is removed and the animation has yet to stop.
                //
                // To workaround this, manually try to find the applicable sublayer that WTUI adds the animation to, and remove it directly.
                // FIXME: This is a fragile workaround, and should be removed once WTUI has proper support for removing effects at any point.
                for sublayer in self.wrapped.layer?.sublayers ?? [] {
                    sublayer.removeAnimation(forKey: "opacity")
                }
            }

            self.wrappedEffectIDToPlatformEffects[wrappedEffectID] = nil
            self.wrapped.removeEffect(wrappedEffectID)
        }
#endif
    }

    /// Removes all currently active effects.
    func removeAllEffects() {
        self.wrapped.removeAllEffects()
        self.platformEffectIDToWrappedEffectIDs = [:]
        self.wrappedEffectIDToPlatformEffects = [:]
    }
}

/// An effect which shifts the remaining text (the text after the currently replaced text) either up or down,
/// depending on if the newly replaced text is taller than the source text.
@MainActor class PlatformIntelligenceRemainderAffordanceTextEffect<Chunk>: PlatformIntelligenceTextEffect where Chunk: PlatformIntelligenceTextEffectChunk {
    private enum AnimationKind {
        case contract
        case expand
    }

    struct Previews {
        let source: PlatformTextPreview?
        let destination: PlatformTextPreview
        let remainder: PlatformContentPreview
    }

    let id = PlatformIntelligenceTextEffectID()
    let chunk: Chunk
    let previews: Previews

    init(chunk: Chunk, previews: Previews) {
        self.chunk = chunk
        self.previews = previews
    }

    private static func animation(for kind: AnimationKind) -> SwiftUI.Animation {
        // Empirically derived animation values, specifically ensuring that the text avoids being overlapped by the replacement animation.

        switch kind {
        case .contract: .easeInOut(duration: 0.4).delay(0.5)
        case .expand: .bouncy(duration: 0.4).delay(0.2)
        }
    }

    private func heightDelta() -> Double {
#if canImport(UIKit)
        let sourceRect = previews.source?.size ?? .zero
        let destRect = previews.destination.size

        let delta = destRect.height - sourceRect.height
#else
        let sourceRect = (previews.source ?? [])
            .map(\.presentationFrame)
            .reduce(CGRect.zero) { $0.union($1) }

        let destRect = previews.destination
            .map(\.presentationFrame)
            .reduce(CGRect.zero) { $0.union($1) }

        let delta = destRect.size.height - sourceRect.size.height
#endif

        return delta
    }

    func _add<Source>(to view: PlatformIntelligenceTextEffectView<Source>) async where Source : PlatformIntelligenceTextEffectViewSource, Source.Chunk == Chunk {
        guard let remainderPreviewImage = previews.remainder.previewImage else {
            return
        }

        // Compute the difference in height between the original text and the replaced text.
        let delta = heightDelta()

        // Create two rects:
        // 1. A source frame for the remainder of the text content before the text is replaced.
        // 2. A destination frame for the remainder of the text content after the text is replaced.
        //
        // The y-coordinates are adjusted for AppKit to account for the origin being the bottom left instead of top left.

        let remainderRect = previews.remainder.presentationFrame

#if canImport(UIKit)
        let remainderViewSourceFrameY = remainderRect.origin.y
#else
        // origin-y-coordinate is flipped in AppKit
        let remainderViewSourceFrameY = view.frame.size.height - remainderRect.size.height - remainderRect.origin.y
#endif

        let remainderViewSourceFrame = CGRect(
            x: remainderRect.origin.x,
            y: remainderViewSourceFrameY,
            width: remainderRect.size.width,
            height: remainderRect.size.height
        )

#if canImport(UIKit)
        // shift down if the replaced text is taller than the source text
        let remainderViewDestFrameY = remainderViewSourceFrame.origin.y + delta
#else
        // shift down if the replaced text is taller than the source text
        let remainderViewDestFrameY = remainderViewSourceFrame.origin.y - delta
#endif

        let remainderViewDestinationFrame = CGRect(
            x: remainderViewSourceFrame.origin.x,
            y: remainderViewDestFrameY,
            width: remainderViewSourceFrame.size.width,
            height: remainderViewSourceFrame.size.height
        )

        // Create an empty view with the source frame, and set its layer's contents to the image
        // of the remaining text content.

        let remainderView = PlatformView(frame: remainderViewSourceFrame)

#if canImport(UIKit)
        remainderView.layer.contents = remainderPreviewImage
#else
        remainderView.wantsLayer = true
        remainderView.layer!.contents = remainderPreviewImage
#endif

        // Add the newly created view as a subview to the effect view.

        view.addSubview(remainderView)

        // Perform the animation to animate the frame to make room for the replaced text.
        // This will run concurrently with the replacement effect, and it must not ever overlap with the effect.

        let animation = Self.animation(for: delta > 0 ? .expand : .contract)
        let changes = {
            remainderView.frame = remainderViewDestinationFrame
        }

#if canImport(UIKit)
        UIView.animate(animation, changes: changes)
#else
        NSAnimationContext.animate(animation, changes: changes)
#endif
    }
}

/// A replacement effect, which essentially involves the original text fading away while at the same time the new text fades in right above it.
@MainActor class PlatformIntelligenceReplacementTextEffect<Chunk>: PlatformIntelligenceTextEffect where Chunk: PlatformIntelligenceTextEffectChunk {
    let id = PlatformIntelligenceTextEffectID()
    let chunk: Chunk

    // This is needed to keep track of when the entire replacement effect has completed,
    // since it is not guaranteed that the source completion handler is always invoked prior
    // to the destination effect completion handler.
    private var hasCompletedPartialWrappedEffect = false

    init(chunk: Chunk) {
        self.chunk = chunk
    }

#if canImport(AppKit)
    private func didCompletePartialWrappedEffect<Source>(for source: Source) where Source: PlatformIntelligenceTextEffectViewSource, Source.Chunk == Chunk {
        if self.hasCompletedPartialWrappedEffect {
            Task { @MainActor in
                await source.replacementEffectDidComplete(self)
            }
        }

        self.hasCompletedPartialWrappedEffect = true
    }
#endif

    func _add<Source>(to view: PlatformIntelligenceTextEffectView<Source>) async where Source : PlatformIntelligenceTextEffectViewSource, Source.Chunk == Chunk {
        // The WT interfaces expect the replacement operation to be performed synchronously, else the source
        // and destination effects become disjoint and begin at different times.
        //
        // To workaround this, the replacement is performed immediately (with the text hidden prior so that
        // it is not visible to the user) before the actual effects begins. The source preview is generated
        // prior to this, and the destination preview is generated after. This allows the previews to be cached
        // so that they can later be retrieved by the WT interface delegates.

        let sourcePreview = await view.source.textPreview(for: self.chunk)

        await view.source.updateTextChunkVisibility(self.chunk, visible: false)

        let (destinationPreview, remainderPreview) = await view.source.performReplacementAndGeneratePreview(for: self.chunk, effect: self)

        guard let destinationPreview else {
            assertionFailure("Failed to generate destination text preview for replacement effect")
            return
        }

#if canImport(UIKit)
        let chunkAdapter = UIReplacementTextEffectTextChunkAdapter(wrapping: self.chunk, source: sourcePreview, destination: destinationPreview)

        let delegateAdapter = UIReplacementTextEffectDelegateAdapter(wrapping: view.source, view: view)
        let wrappedEffect = UITextEffectView.ReplacementTextEffect(chunk: chunkAdapter, view: view.wrapped, delegate: delegateAdapter)

        await view.source.replacementEffectWillBegin(self)

        view.wrapped.addEffect(wrappedEffect)
        view.wrappedEffectIDToPlatformEffects[wrappedEffect.id] = self
        view.platformEffectIDToWrappedEffectIDs[self.id] = wrappedEffect.id
#else
        // The WTUI interface on macOS exposes the replacement effect as two separate effects, a source effect
        // and a destination effect. To abstract this disparity between the platforms, the effects are modeled
        // as a single replacement effect, to match the iOS interface and provide a cohesive API.

        let sourceChunkAdapter = WTTextChunkAdapter(wrapping: self.chunk, preview: sourcePreview)
        let destinationChunkAdapter = WTTextChunkAdapter(wrapping: self.chunk, preview: destinationPreview)

        let wrappedDestinationEffect = _WTReplaceTextEffect(chunk: destinationChunkAdapter, effectView: view.wrapped)
        wrappedDestinationEffect.isDestination = true
        wrappedDestinationEffect.animateRemovalWhenDone = true

        wrappedDestinationEffect.completion = {
            // The destination completion handler is invoked right before it starts its opacity animation.
            self.didCompletePartialWrappedEffect(for: view.source)
        }

        let wrappedSourceEffect = _WTReplaceTextEffect(chunk: sourceChunkAdapter, effectView: view.wrapped)
        wrappedSourceEffect.animateRemovalWhenDone = false

        wrappedSourceEffect.preCompletion = {
            // This block is invoked after the source effect has been prepared, but before it actually begins.
            // It's intended for the destination effect to be added here, synchronously.

            let destinationEffectID = view.wrapped.add(wrappedDestinationEffect)!
            view.wrappedEffectIDToPlatformEffects[destinationEffectID] = self
            view.platformEffectIDToWrappedEffectIDs[self.id, default: []].insert(destinationEffectID)
        }

        wrappedSourceEffect.completion = {
            // The source completion handler is invoked right after it ends its opacity animation.
            self.didCompletePartialWrappedEffect(for: view.source)
        }

        await view.source.replacementEffectWillBegin(self)

        let sourceEffectID = view.wrapped.add(wrappedSourceEffect)!
        view.wrappedEffectIDToPlatformEffects[sourceEffectID] = self
        view.platformEffectIDToWrappedEffectIDs[self.id, default: []].insert(sourceEffectID)
#endif

        guard let remainderPreview else {
            return
        }

        let previews = PlatformIntelligenceRemainderAffordanceTextEffect<Chunk>.Previews(source: sourcePreview, destination: destinationPreview, remainder: remainderPreview)
        let remainderEffect = PlatformIntelligenceRemainderAffordanceTextEffect(chunk: chunk, previews: previews)
        await remainderEffect._add(to: view)
    }
}

/// An effect which adds a shimmer animation to some text, intended to indicate that some operation is pending.
class PlatformIntelligencePonderingTextEffect<Chunk>: PlatformIntelligenceTextEffect where Chunk: PlatformIntelligenceTextEffectChunk {
#if canImport(UIKit)
    private typealias ChunkAdapter = UIPonderingTextEffectTextChunkAdapter
#else
    private typealias ChunkAdapter = WTTextChunkAdapter
#endif

    let id = PlatformIntelligenceTextEffectID()
    let chunk: Chunk

    init(chunk: Chunk) {
        self.chunk = chunk
    }

    func _add<Source>(to view: PlatformIntelligenceTextEffectView<Source>) async where Source : PlatformIntelligenceTextEffectViewSource, Source.Chunk == Chunk {
        guard let preview = await view.source.textPreview(for: self.chunk) else {
            assertionFailure("Failed to generate text preview for pondering effect")
            return
        }

        let chunkAdapter = ChunkAdapter(wrapping: self.chunk, preview: preview)

#if canImport(UIKit)
        let wrappedEffect = UITextEffectView.PonderingEffect(chunk: chunkAdapter, view: view.wrapped)
        view.wrapped.addEffect(wrappedEffect)

        view.wrappedEffectIDToPlatformEffects[wrappedEffect.id] = self
        view.platformEffectIDToWrappedEffectIDs[self.id] = wrappedEffect.id
#else
        let wrappedEffect = _WTSweepTextEffect(chunk: chunkAdapter, effectView: view.wrapped)
        view.wrapped.add(wrappedEffect)

        view.wrappedEffectIDToPlatformEffects[wrappedEffect.identifier] = self
        view.platformEffectIDToWrappedEffectIDs[self.id] = [wrappedEffect.identifier]
#endif
    }
}

#endif