File: TextAnimationManager.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 (142 lines) | stat: -rw-r--r-- 5,567 bytes parent folder | download | duplicates (7)
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
//
// Copyright (C) 2024 Apple Inc. All rights reserved.
//

#if canImport(WritingTools) && canImport(UIKit)

import OSLog
import WebKit
import WebKitSwift
internal import UIKit_Private
@_spi(TextEffects) import UIKit

@objc public enum WKTextAnimationType: Int {
    case initial
    case source
    case final
}

@objc(WKSTextAnimationManager)
@MainActor public final class TextAnimationManager: NSObject {
    private static let logger = Logger(subsystem: "com.apple.WebKit", category: "TextAnimationType")
    
    final class TextEffectChunk: UITextEffectTextChunk {
        public let uuid: UUID
        
        public init(uuid: UUID) {
            self.uuid = uuid
        }
    }
    
    private var currentEffect: UITextEffectView.EffectID?
    private lazy var effectView = UITextEffectView(source: self)
    private var chunkToEffect = [UUID: UITextEffectView.EffectID]()
    
    @objc public weak var delegate: WKSTextAnimationSourceDelegate?

    @objc(initWithDelegate:) public init(with delegate: any WKSTextAnimationSourceDelegate) {
        super.init()
        
        self.delegate = delegate
        delegate.containingViewForTextAnimationType().addSubview(self.effectView)
    }
    
    @objc(addTextAnimationForAnimationID:withStyleType:) public func beginEffect(for uuid: UUID, style: WKTextAnimationType) {
        switch style {
        case .initial:
            let newEffect = self.effectView.addEffect(UITextEffectView.PonderingEffect(chunk: TextEffectChunk(uuid: uuid), view: self.effectView) as UITextEffectView.TextEffect)
            self.chunkToEffect[uuid] = newEffect
        case .source:
            let newEffect = self.effectView.addEffect(UITextEffectView.ReplacementTextEffect(chunk: TextEffectChunk(uuid: uuid), view: self.effectView, delegate:self) as UITextEffectView.TextEffect)
            self.chunkToEffect[uuid] = newEffect
        case .final:
            break
            // Discard `.final` since we don't manually start the 2nd part of the animation on iOS.
        }
    }
    
    @objc(removeTextAnimationForAnimationID:) public func endEffect(for uuid: UUID) {
        if let effectID = chunkToEffect.removeValue(forKey: uuid) {
            self.effectView.removeEffect(effectID)
        }
    }
}
    
@_spi(TextEffects)
extension TextAnimationManager: UITextEffectViewSource {
    public func targetedPreview(for chunk: UITextEffectTextChunk) async -> UITargetedPreview {
        
        guard let delegate = self.delegate else {
            Self.logger.debug("Can't obtain Targeted Preview. Missing delegate." )
            return UITargetedPreview(view: UIView(frame: .zero))
        }
        
        let defaultPreview = UITargetedPreview(view: UIView(frame: .zero), parameters: UIPreviewParameters(), target: UIPreviewTarget(container: delegate.containingViewForTextAnimationType(), center: delegate.containingViewForTextAnimationType().center))
        guard let uuidChunk = chunk as? TextEffectChunk else {
            Self.logger.debug("Can't get text preview. Incorrect UITextEffectTextChunk subclass")
            return defaultPreview
        }

        guard let preview = await delegate.targetedPreview(for: uuidChunk.uuid) else {
            Self.logger.debug("Could not generate a UITargetedPreview")
            return defaultPreview
        }

        return preview
    }
    
    public func updateTextChunkVisibilityForAnimation(_ chunk: UITextEffectTextChunk, visible: Bool) async {
        guard let uuidChunk = chunk as? TextEffectChunk else {
            Self.logger.debug("Can't update text visibility. Incorrect UITextEffectTextChunk subclass")
            return
        }
        guard let delegate = self.delegate else {
            Self.logger.debug("Can't update Chunk Visibility. Missing delegate." )
            return
        }
        await delegate.updateUnderlyingTextVisibility(forTextAnimationID:uuidChunk.uuid, visible: visible)
    }
}

@_spi(TextEffects)
extension TextAnimationManager: UITextEffectView.ReplacementTextEffect.Delegate {
    public func performReplacementAndGeneratePreview(for chunk: UITextEffectTextChunk, effect: UITextEffectView.ReplacementTextEffect, animation: UITextEffectView.ReplacementTextEffect.AnimationParameters) async -> UITargetedPreview? {
        guard let uuidChunk = chunk as? TextEffectChunk else {
            Self.logger.debug("Can't get text preview. Incorrect UITextEffectTextChunk subclass")
            return nil
        }

        guard let delegate = self.delegate else {
            Self.logger.debug("Can't obtain Targeted Preview. Missing delegate." )
            return nil
        }

        let preview = await withCheckedContinuation { continuation in
            delegate.callCompletionHandler(forAnimationID: uuidChunk.uuid) { preview in
                continuation.resume(returning: preview)
            }
        }

        return preview
    }
    
    public func replacementEffectDidComplete(_ effect: UITextEffectView.ReplacementTextEffect) {
        self.effectView.removeEffect(effect.id)

        guard let (animationID, _) = self.chunkToEffect.first(where: { (_, value) in value == effect.id }) else {
            return
        }

        self.chunkToEffect[animationID] = nil

        guard let delegate = self.delegate else {
            Self.logger.debug("Missing delegate.")
            return
        }

        delegate.callCompletionHandler(forAnimationID: animationID)
        delegate.replacementEffectDidComplete();
    }
}

#endif // canImport(WritingTools) && canImport(UIKit)