diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index a396ccec59..b8c8702054 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -11,6 +11,11 @@ import SwiftyGif import SimpleXChat import PhotosUI +private let defaultAttributes: [NSAttributedString.Key : Any] = [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: UIColor.label +] + struct NativeTextEditor: UIViewRepresentable { @Binding var text: String @Binding var disableEditing: Bool @@ -29,24 +34,31 @@ struct NativeTextEditor: UIViewRepresentable { func makeUIView(context: Context) -> UITextView { let field = CustomUITextField(height: _height) field.backgroundColor = .clear - field.text = text + field.attributedText = NSAttributedString( + string: "", + attributes: [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: UIColor.label + ] + ) field.textAlignment = alignment(text) field.autocapitalizationType = .sentences - field.setOnTextChangedListener { newText, images in - if !disableEditing { - text = newText - field.textAlignment = alignment(text) - updateFont(field) - // Speed up the process of updating layout, reduce jumping content on screen - updateHeight(field) - self.height = field.frame.size.height - } else { - field.text = text - } - if !images.isEmpty { - onImagesAdded(images) - } - } +// TODO: Reintegrate with attributed string +// field.setOnTextChangedListener { newText, images in +// if !disableEditing { +// text = newText +// field.textAlignment = alignment(text) +// updateFont(field) +// // Speed up the process of updating layout, reduce jumping content on screen +// updateHeight(field) +// self.height = field.frame.size.height +// } else { +// field.text = text +// } +// if !images.isEmpty { +// onImagesAdded(images) +// } +// } field.setOnFocusChangedListener { focused = $0 } field.delegate = field field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) @@ -57,7 +69,8 @@ struct NativeTextEditor: UIViewRepresentable { func updateUIView(_ field: UITextView, context: Context) { if field.markedTextRange == nil && field.text != text { - field.text = text +// TODO: Reintegrate with attributed string +// field.text = text field.textAlignment = alignment(text) updateFont(field) updateHeight(field) @@ -173,33 +186,34 @@ private class CustomUITextField: UITextView, UITextViewDelegate { } func textViewDidChange(_ textView: UITextView) { - if textView.markedTextRange == nil { - var images: [UploadContent] = [] - var rangeDiff = 0 - let newAttributedText = NSMutableAttributedString(attributedString: textView.attributedText) - textView.attributedText.enumerateAttribute( - NSAttributedString.Key.attachment, - in: NSRange(location: 0, length: textView.attributedText.length), - options: [], - using: { value, range, _ in - if let attachment = (value as? NSTextAttachment)?.fileWrapper?.regularFileContents { - do { - images.append(.animatedImage(image: try UIImage(gifData: attachment))) - } catch { - if let img = (value as? NSTextAttachment)?.image { - images.append(.simpleImage(image: img)) - } - } - newAttributedText.replaceCharacters(in: NSMakeRange(range.location - rangeDiff, range.length), with: "") - rangeDiff += range.length - } - } - ) - if textView.attributedText != newAttributedText { - textView.attributedText = newAttributedText - } - onTextChanged(textView.text, images) - } +// TODO: Reintegrate with attributed string +// if textView.markedTextRange == nil { +// var images: [UploadContent] = [] +// var rangeDiff = 0 +// let newAttributedText = NSMutableAttributedString(attributedString: textView.attributedText) +// textView.attributedText.enumerateAttribute( +// NSAttributedString.Key.attachment, +// in: NSRange(location: 0, length: textView.attributedText.length), +// options: [], +// using: { value, range, _ in +// if let attachment = (value as? NSTextAttachment)?.fileWrapper?.regularFileContents { +// do { +// images.append(.animatedImage(image: try UIImage(gifData: attachment))) +// } catch { +// if let img = (value as? NSTextAttachment)?.image { +// images.append(.simpleImage(image: img)) +// } +// } +// newAttributedText.replaceCharacters(in: NSMakeRange(range.location - rangeDiff, range.length), with: "") +// rangeDiff += range.length +// } +// } +// ) +// if textView.attributedText != newAttributedText { +// textView.attributedText = newAttributedText +// } +// onTextChanged(textView.text, images) +// } } func textViewDidBeginEditing(_ textView: UITextView) { @@ -211,12 +225,37 @@ private class CustomUITextField: UITextView, UITextViewDelegate { } // MARK: Formatting + + /// Toggles an attribute in the currently selected text range + /// - Parameters: + /// - attribute: Value of the attribute to be enabled or disabled + /// - key: Key for which to apply the attribute + /// - detect: Block which detects, if the attribute is already present within the selection + func toggle( + _ attribute: Any, + for key: NSAttributedString.Key, + detect: (Any) -> Bool? + ) { + var detected = false + textStorage.enumerateAttribute(key, in: selectedRange) { value, _, stop in + if let value, detect(value) == true { + detected = true + stop.pointee = true + } + } + + if key == .backgroundColor { + textStorage.removeAttribute(.backgroundColor, range: selectedRange) + } else { + textStorage.setAttributes(defaultAttributes, range: selectedRange) + } + + if !detected { + textStorage.addAttribute(key, value: attribute, range: selectedRange) + } + } private var formatMenu: UIMenu { - func set(_ attribute: Any, for key: NSAttributedString.Key) { - textStorage.setAttributes([key: attribute], range: selectedRange) - delegate?.textViewDidChange?(self) - } func systemImage(_ systemName: String, color: UIColor? = nil) -> UIImage? { if let uiImage = UIImage(systemName: systemName) { @@ -228,23 +267,53 @@ private class CustomUITextField: UITextView, UITextViewDelegate { } else { nil } } + var bodySize: CGFloat { UIFont.preferredFont(forTextStyle: .body).pointSize } + + + let colors: [UIColor] = [.red, .green, .blue, .yellow, .cyan, .magenta] + return UIMenu( title: "Format", children: [ - UIAction(image: systemImage("bold")) { _ in /*TODO*/ }, - UIAction(image: systemImage("italic")) { _ in /*TODO*/ }, - UIAction(image: systemImage("strikethrough")) { _ in /*TODO*/ }, - UIAction(title: "Mono") { _ in /*TODO*/ }, + UIAction(image: systemImage("bold")) { _ in + self.toggle(UIFont.boldSystemFont(ofSize: bodySize), for: .font) { value in + (value as? UIFont)?.fontDescriptor.symbolicTraits.contains(.traitBold) + } + }, + UIAction(image: systemImage("italic")) { _ in + self.toggle(UIFont.italicSystemFont(ofSize: bodySize), for: .font) { value in + (value as? UIFont)?.fontDescriptor.symbolicTraits.contains(.traitItalic) + } + }, + UIAction(image: systemImage("strikethrough")) { _ in + self.toggle(NSUnderlineStyle.single.rawValue, for: .strikethroughStyle) { value in + (value as? Int).map { $0 == NSUnderlineStyle.single.rawValue } + } + }, + UIAction(title: "Mono") { _ in + self.toggle(UIFont.monospacedSystemFont(ofSize: bodySize, weight: .regular), for: .font) { value in + (value as? UIFont)?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace) + } + }, UIMenu( title: "Color", - children: Array([.red, .green, .blue, .yellow, .cyan, .magenta]) + children: colors .map { color in UIAction(image: systemImage("circle.fill", color: color)) { _ in - set(color, for: .foregroundColor) + self.toggle(color, for: .foregroundColor) { value in + (value as? UIColor).map { $0 == color } + } } } ), - UIAction(title: "Secret") { test in /*TODO*/ } + UIAction(title: "Secret") { _ in + self.toggle(UIColor.clear, for: .foregroundColor) { value in + (value as? UIColor).map { $0 == .clear } + } + self.toggle(UIColor.secondarySystemFill, for: .backgroundColor) { value in + (value as? UIColor).map { $0 == .secondarySystemFill } + } + } ] ) }