Files
Pulse/Sources/PulseUI/Features/FileViewer/RichTextView/WrappedTextView.swift
2024-07-14 13:14:00 -04:00

152 lines
4.8 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
#if os(iOS) || os(macOS) || os(visionOS)
import SwiftUI
import Combine
#if os(iOS) || os(visionOS)
struct WrappedTextView: UIViewRepresentable {
let viewModel: RichTextViewModel
@ObservedObject private var settings = UserSettings.shared
final class Coordinator: NSObject, UITextViewDelegate {
var onLinkTapped: ((URL) -> Bool)?
var cancellables: [AnyCancellable] = []
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if let onLinkTapped = onLinkTapped, onLinkTapped(URL) {
return false
}
if let (title, message) = parseTooltip(URL) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(.init(title: "Done", style: .cancel))
UIApplication.keyWindow?.rootViewController?.present(alert, animated: true)
return false
}
return true
}
}
func makeUIView(context: Context) -> UXTextView {
let textView: UITextView
if #available(iOS 16, *) {
// Disables the new TextKit 2 which is extremely slow on iOS 16
textView = UITextView(usingTextLayoutManager: false)
} else {
textView = UITextView()
}
configureTextView(textView)
textView.delegate = context.coordinator
textView.attributedText = viewModel.originalText
viewModel.textView = textView
return textView
}
func updateUIView(_ textView: UXTextView, context: Context) {
textView.isAutomaticLinkDetectionEnabled = settings.isLinkDetectionEnabled && viewModel.isLinkDetectionEnabled
}
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator()
coordinator.onLinkTapped = viewModel.onLinkTapped
return coordinator
}
}
#elseif os(macOS)
struct WrappedTextView: NSViewRepresentable {
let viewModel: RichTextViewModel
@ObservedObject private var settings = UserSettings.shared
final class Coordinator: NSObject, NSTextViewDelegate {
var onLinkTapped: ((URL) -> Bool)?
var cancellables: [AnyCancellable] = []
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
guard let url = link as? URL else {
return false
}
if let onLinkTapped = onLinkTapped, onLinkTapped(url) {
return true
}
return false
}
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = UXTextView.scrollableTextView()
let textView = scrollView.documentView as! UXTextView
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = true
configureTextView(textView)
textView.delegate = context.coordinator
textView.attributedText = viewModel.originalText
viewModel.textView = textView
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
let textView = scrollView.documentView as! NSTextView
textView.isAutomaticLinkDetectionEnabled = settings.isLinkDetectionEnabled && viewModel.isLinkDetectionEnabled
}
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator()
coordinator.onLinkTapped = viewModel.onLinkTapped
return coordinator
}
}
#endif
private func configureTextView(_ textView: UXTextView) {
textView.isSelectable = true
textView.isEditable = false
textView.linkTextAttributes = [
.underlineStyle: 1
]
textView.backgroundColor = .clear
#if os(iOS) || os(visionOS)
textView.alwaysBounceVertical = true
textView.autocorrectionType = .no
textView.autocapitalizationType = .none
textView.adjustsFontForContentSizeCategory = true
textView.textContainerInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10)
#endif
#if os(iOS)
textView.keyboardDismissMode = .interactive
#endif
#if os(macOS)
textView.isAutomaticSpellingCorrectionEnabled = false
textView.textContainerInset = NSSize(width: 10, height: 10)
#endif
}
private func parseTooltip(_ url: URL) -> (title: String?, message: String)? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.scheme == "pulse",
components.path == "tooltip",
let queryItems = components.queryItems,
let message = queryItems.first(where: { $0.name == "message" })?.value else {
return nil
}
let title = queryItems.first(where: { $0.name == "title" })?.value
return (title: title, message: message)
}
#endif