diff --git a/RichEditorSwiftUI.podspec b/RichEditorSwiftUI.podspec index 0c50a63..e8afa4e 100644 --- a/RichEditorSwiftUI.podspec +++ b/RichEditorSwiftUI.podspec @@ -7,25 +7,24 @@ Pod::Spec.new do |s| Wrapper around UITextView to support Rich text editing in SwiftUI. DESC - s.homepage = 'https://github.com/canopas/RichEditorSwiftUI' - s.license = { :type => 'MIT', :file => 'LICENSE.md' } - s.author = { 'Jimmy' => 'jimmy@canopas.com' } - s.source = { :git => 'https://github.com/canopas/rich-editor-swiftui.git', :tag => s.version.to_s } - s.social_media_url = 'https://x.com/canopas_eng' + s.homepage = "https://github.com/canopas/RichEditorSwiftUI" + s.license = { :type => "MIT", :file => "LICENSE.md" } + s.author = { "Jimmy" => "jimmy@canopas.com" } + s.source = { :git => "https://github.com/canopas/rich-editor-swiftui.git", :tag => s.version.to_s } + s.social_media_url = "https://x.com/canopas_eng" - s.source_files = 'Sources/**/*.swift' + s.source_files = "Sources/**/*.swift" - s.module_name = 'RichEditorSwiftUI' + s.module_name = "RichEditorSwiftUI" s.requires_arc = true - s.swift_version = '5.9' + s.swift_version = "5.9" - s.ios.deployment_target = '15.0' - s.osx.deployment_target = '12.0' - s.tvos.deployment_target = '17.0' - s.visionos.deployment_target = '1.0' - s.watchos.deployment_target = '8.0' + s.ios.deployment_target = "15.0" + s.osx.deployment_target = "12.0" + s.tvos.deployment_target = "17.0" + s.watchos.deployment_target = "8.0" - s.preserve_paths = 'README.md' + s.preserve_paths = "README.md" end diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift deleted file mode 100644 index 948bbdc..0000000 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// RichTextAttributeWriter+List.swift -// -// -// Created by Divyesh Vekariya on 09/05/24. -// - -import Foundation - -#if canImport(UIKit) - import UIKit -#endif - -#if canImport(AppKit) && !targetEnvironment(macCatalyst) - import AppKit -#endif - -extension RichTextAttributeWriter { - - /** - Set the text alignment at a certain range. - - Unlike some other attributes, this value applies to the - entire paragraph, not just the selected range. - */ - public func setRichTextListStyle( - _ listType: ListType, - to newValue: Bool, - at range: NSRange - ) { - setListStyle(listType, to: newValue, at: range) - } -} - -extension RichTextAttributeWriter { - - fileprivate func setListStyle( - _ listType: ListType, - to newValue: Bool, - at range: NSRange - ) { - guard let string = mutableRichText else { return } - let safeRange = safeRange(for: range) - - let searchRange = NSRange( - location: max(0, (range.location - 1)), - length: min(string.string.utf16Length, (range.length + 1))) - var previousRang: NSRange? = nil - - var attributesWithRange: - [Int: (range: NSRange, paragraphStyle: NSMutableParagraphStyle)] = - [:] - string.beginEditing() - var previousStyle: NSMutableParagraphStyle? = nil - string.enumerateAttribute(.paragraphStyle, in: searchRange) { - (attribute, range, _) in - - if let style = attribute as? NSMutableParagraphStyle, - !style.textLists.isEmpty - { - if newValue { - /// For add style - attributesWithRange[attributesWithRange.count] = ( - range: range, paragraphStyle: style - ) - - if safeRange.location <= range.location - && safeRange.upperBound >= range.upperBound - { - string.removeAttribute(.paragraphStyle, range: range) - } - - if let oldRange = previousRang, - let previousStyle = previousStyle, - previousStyle.textLists.count == listType.getIndent() - { - let location = min(oldRange.location, range.location) - let length = - max(oldRange.upperBound, range.upperBound) - - location - let combinedRange = NSRange( - location: location, length: length) - - string.addAttribute( - .paragraphStyle, value: previousStyle, - range: combinedRange) - previousRang = combinedRange - } else { - let location = min(safeRange.location, range.location) - let length = - max(safeRange.upperBound, range.upperBound) - - location - let combinedRange = NSRange( - location: location, length: length) - - string.addAttribute( - .paragraphStyle, value: style, range: combinedRange) - previousRang = combinedRange - } - previousStyle = style - } else { - /// Fore Remove Style - if safeRange.closedRange.overlaps(range.closedRange) { - if style.textLists.count == listType.getIndent() { - string.removeAttribute( - .paragraphStyle, range: safeRange) - previousRang = nil - previousStyle = nil - } - } - } - } - } - - ///Add style if not already added - if attributesWithRange.isEmpty { - - let paragraphStyle = NSMutableParagraphStyle() - - paragraphStyle.alignment = .left - let listItem = TextList( - markerFormat: listType.getMarkerFormat(), options: 0) - - if paragraphStyle.textLists.isEmpty && newValue { - paragraphStyle.textLists.append(listItem) - } else { - paragraphStyle.textLists.removeAll() - } - - if !paragraphStyle.textLists.isEmpty { - string.addAttributes( - [.paragraphStyle: paragraphStyle], range: safeRange) - } else { - string.removeAttribute(.paragraphStyle, range: safeRange) - } - } - - string.fixAttributes(in: range) - string.endEditing() - } -} diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift deleted file mode 100644 index b1198f2..0000000 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// RichTextAttributeWriter+Style.swift -// -// -// Created by Divyesh Vekariya on 28/12/23. -// - -import Foundation - -extension NSMutableAttributedString { - - /** - Set a rich text style at a certain range. - - The function uses `safeRange(for:)` to handle incorrect - ranges, which is not handled by the native functions. - - - Parameters: - - style: The style to set. - - newValue: The new value to set the attribute to. - - range: The range to affect, by default the entire text. - */ - public func setRichTextStyle( - _ style: RichTextSpanStyle, - to newValue: Bool, - at range: NSRange? = nil - ) { - let rangeValue = range ?? richTextRange - let range = safeRange(for: rangeValue) - - if style.isList, let style = style.listType { - setRichTextListStyle(style, to: newValue, at: range) - } - - guard !style.isList else { return } - - let attributeValue = newValue ? 1 : 0 - if style == .underline { - return setRichTextAttribute( - .underlineStyle, to: attributeValue, at: range) - } - if style == .strikethrough { - return setRichTextAttribute( - .strikethroughStyle, to: attributeValue, at: range) - } - let font = richTextFont(at: range) ?? .standardRichTextFont - let styles = richTextStyles(at: range) - let shouldAdd = newValue && !styles.hasStyle(style) - let shouldRemove = !newValue && styles.hasStyle(style) - guard shouldAdd || shouldRemove || style.isHeaderStyle else { return } - var descriptor = font.fontDescriptor - if let richTextStyle = style.richTextStyle, - !style.isDefault && !style.isHeaderStyle - { - descriptor = descriptor.byTogglingStyle(richTextStyle) - } - let newFont: FontRepresentable? = FontRepresentable( - descriptor: descriptor, - size: byTogglingFontSizeFor( - style: style, font: font, shouldAdd: newValue)) - guard let newFont = newFont else { return } - setRichTextFont(newFont, at: range) - } - - /** - This will reset font size before multiplying new size - */ - private func byTogglingFontSizeFor( - style: RichTextSpanStyle, font: FontRepresentable, shouldAdd: Bool - ) -> CGFloat { - guard style.isHeaderStyle || style.isDefault else { - return font.pointSize - } - - let cleanFont = style.getFontAfterRemovingStyle(font: font) - if shouldAdd { - return cleanFont.pointSize * style.fontSizeMultiplier - } else { - return font.pointSize / style.fontSizeMultiplier - } - } -} diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift index c35f957..cbda37b 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift @@ -5,56 +5,56 @@ // Created by Divyesh Vekariya on 21/10/24. // #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) - import Combine - import SwiftUI + import Combine + import SwiftUI - /// This coordinator is used to keep a ``RichTextView`` in sync - /// with a ``RichEditorState``. - /// - /// This is used by ``RichTextEditor`` to coordinate changes in - /// its context and the underlying text view. - /// - /// The coordinator sets itself as the text view's delegate. It - /// updates the context when things change in the text view and - /// syncs to context changes to the text view. - open class RichTextCoordinator: NSObject { + /// This coordinator is used to keep a ``RichTextView`` in sync + /// with a ``RichEditorState``. + /// + /// This is used by ``RichTextEditor`` to coordinate changes in + /// its context and the underlying text view. + /// + /// The coordinator sets itself as the text view's delegate. It + /// updates the context when things change in the text view and + /// syncs to context changes to the text view. + open class RichTextCoordinator: NSObject { - // MARK: - Properties + // MARK: - Properties - /// The rich text context to coordinate with. - public let context: RichEditorState + /// The rich text context to coordinate with. + public let context: RichEditorState - /// The rich text to edit. - public var text: Binding + /// The rich text to edit. + public var text: Binding - /// The text view for which the coordinator is used. - public private(set) var textView: RichTextView + /// The text view for which the coordinator is used. + public private(set) var textView: RichTextView - /// This set is used to store context observations. - public var cancellables = Set() + /// This set is used to store context observations. + public var cancellables = Set() - /// This flag is used to avoid delaying context sync. - var shouldDelaySyncContextWithTextView = false + /// This flag is used to avoid delaying context sync. + var shouldDelaySyncContextWithTextView = false - // MARK: - Internal Properties + // MARK: - Internal Properties - /** + /** The background color that was used before the currently highlighted range was set. */ - var highlightedRangeOriginalBackgroundColor: ColorRepresentable? + var highlightedRangeOriginalBackgroundColor: ColorRepresentable? - /** + /** The foreground color that was used before the currently highlighted range was set. */ - var highlightedRangeOriginalForegroundColor: ColorRepresentable? + var highlightedRangeOriginalForegroundColor: ColorRepresentable? - private var cancellable: Set = [] + private var cancellable: Set = [] - // MARK: - Initialization + // MARK: - Initialization - /** + /** Create a rich text coordinator. - Parameters: @@ -62,211 +62,211 @@ - textView: The rich text view to keep in sync. - richEditorState: The context to keep in sync. */ - public init( - text: Binding, - textView: RichTextView, - richTextContext: RichEditorState - ) { - textView.attributedString = text.wrappedValue - self.text = text - self.textView = textView - self.context = richTextContext - super.init() - self.textView.delegate = self - subscribeToUserActions() - } - #if canImport(UIKit) - - // MARK: - UITextViewDelegate - - open func textViewDidBeginEditing(_ textView: UITextView) { - context.onTextViewEvent( - .didBeginEditing( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - context.isEditingText = true - } - - open func textViewDidChange(_ textView: UITextView) { - context.onTextViewEvent( - .didChange( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - syncWithTextView() - } - - open func textViewDidChangeSelection(_ textView: UITextView) { - context.onTextViewEvent( - .didChangeSelection( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - syncWithTextView() - } - - open func textViewDidEndEditing(_ textView: UITextView) { - context.onTextViewEvent( - .didEndEditing( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - context.isEditingText = false - syncWithTextView() - } - #endif - - #if canImport(AppKit) && !targetEnvironment(macCatalyst) - - // MARK: - NSTextViewDelegate - - open func textDidBeginEditing(_ notification: Notification) { - context.onTextViewEvent( - .didBeginEditing( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) - context.isEditingText = true - } - - open func textDidChange(_ notification: Notification) { - context.onTextViewEvent( - .didChange( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) - syncWithTextView() - } - - open func textViewDidChangeSelection(_ notification: Notification) { - replaceCurrentAttributesIfNeeded() - context.onTextViewEvent( - .didChangeSelection( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) - syncWithTextView() - } - - open func textDidEndEditing(_ notification: Notification) { - context.onTextViewEvent( - .didEndEditing( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) - context.isEditingText = false - } - #endif + public init( + text: Binding, + textView: RichTextView, + richTextContext: RichEditorState + ) { + textView.attributedString = text.wrappedValue + self.text = text + self.textView = textView + self.context = richTextContext + super.init() + self.textView.delegate = self + subscribeToUserActions() } + #if canImport(UIKit) + + // MARK: - UITextViewDelegate + + open func textViewDidBeginEditing(_ textView: UITextView) { + context.onTextViewEvent( + .didBeginEditing( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + context.isEditingText = true + } + + open func textViewDidChange(_ textView: UITextView) { + context.onTextViewEvent( + .didChange( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + syncWithTextView() + } + + open func textViewDidChangeSelection(_ textView: UITextView) { + context.onTextViewEvent( + .didChangeSelection( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + syncWithTextView() + } + + open func textViewDidEndEditing(_ textView: UITextView) { + context.onTextViewEvent( + .didEndEditing( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + context.isEditingText = false + syncWithTextView() + } + #endif - #if os(iOS) || os(tvOS) || os(visionOS) - import UIKit + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + + // MARK: - NSTextViewDelegate + + open func textDidBeginEditing(_ notification: Notification) { + context.onTextViewEvent( + .didBeginEditing( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) + context.isEditingText = true + } + + open func textDidChange(_ notification: Notification) { + context.onTextViewEvent( + .didChange( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) + syncWithTextView() + } + + open func textViewDidChangeSelection(_ notification: Notification) { + replaceCurrentAttributesIfNeeded() + context.onTextViewEvent( + .didChangeSelection( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) + syncWithTextView() + } + + open func textDidEndEditing(_ notification: Notification) { + context.onTextViewEvent( + .didEndEditing( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) + context.isEditingText = false + } + #endif + } - extension RichTextCoordinator: UITextViewDelegate {} + #if os(iOS) || os(tvOS) || os(visionOS) + import UIKit - #elseif macOS - import AppKit + extension RichTextCoordinator: UITextViewDelegate {} - extension RichTextCoordinator: NSTextViewDelegate {} - #endif + #elseif os(macOS) + import AppKit - // MARK: - Public Extensions + extension RichTextCoordinator: NSTextViewDelegate {} + #endif - extension RichTextCoordinator { + // MARK: - Public Extensions - /// Reset appearance for the currently highlighted range. - public func resetHighlightedRangeAppearance() { - guard - let range = context.highlightedRange, - let background = highlightedRangeOriginalBackgroundColor, - let foreground = highlightedRangeOriginalForegroundColor - else { return } - textView.setRichTextColor(.background, to: background, at: range) - textView.setRichTextColor(.foreground, to: foreground, at: range) - } + extension RichTextCoordinator { + + /// Reset appearance for the currently highlighted range. + public func resetHighlightedRangeAppearance() { + guard + let range = context.highlightedRange, + let background = highlightedRangeOriginalBackgroundColor, + let foreground = highlightedRangeOriginalForegroundColor + else { return } + textView.setRichTextColor(.background, to: background, at: range) + textView.setRichTextColor(.foreground, to: foreground, at: range) } + } - // MARK: - Internal Extensions + // MARK: - Internal Extensions - extension RichTextCoordinator { + extension RichTextCoordinator { - /// Sync state from the text view's current state. - func syncWithTextView() { - syncContextWithTextView() - syncTextWithTextView() - } + /// Sync state from the text view's current state. + func syncWithTextView() { + syncContextWithTextView() + syncTextWithTextView() + } - /// Sync the rich text context with the text view. - func syncContextWithTextView() { - if shouldDelaySyncContextWithTextView { - DispatchQueue.main.async { - self.syncContextWithTextViewAfterDelay() - } - } else { - syncContextWithTextViewAfterDelay() - } + /// Sync the rich text context with the text view. + func syncContextWithTextView() { + if shouldDelaySyncContextWithTextView { + DispatchQueue.main.async { + self.syncContextWithTextViewAfterDelay() } + } else { + syncContextWithTextViewAfterDelay() + } + } - func sync(_ prop: inout T, with value: T) { - if prop == value { return } - prop = value - } + func sync(_ prop: inout T, with value: T) { + if prop == value { return } + prop = value + } - /// Sync the rich text context with the text view. - func syncContextWithTextViewAfterDelay() { - let font = textView.richTextFont ?? .standardRichTextFont - sync(&context.attributedString, with: textView.attributedString) - sync(&context.selectedRange, with: textView.selectedRange) - sync(&context.canCopy, with: textView.hasSelectedRange) - sync( - &context.canRedoLatestChange, - with: textView.undoManager?.canRedo ?? false) - sync( - &context.canUndoLatestChange, - with: textView.undoManager?.canUndo ?? false) - sync(&context.fontName, with: font.fontName) - sync(&context.fontSize, with: font.pointSize) - sync(&context.isEditingText, with: textView.isFirstResponder) - sync( - &context.paragraphStyle, - with: textView.richTextParagraphStyle ?? .default) - sync( - &context.textAlignment, - with: textView.richTextAlignment ?? .left) - sync(&context.link, with: textView.richTextLink) - - RichTextColor.allCases.forEach { - if let color = textView.richTextColor($0) { - context.setColor($0, to: color) - } - } - - let styles = textView.richTextStyles - RichTextStyle.allCases.forEach { - let style = styles.hasStyle($0) - context.setStyleInternal($0, to: style) - } - - updateTextViewAttributesIfNeeded() + /// Sync the rich text context with the text view. + func syncContextWithTextViewAfterDelay() { + let font = textView.richTextFont ?? .standardRichTextFont + sync(&context.attributedString, with: textView.attributedString) + sync(&context.selectedRange, with: textView.selectedRange) + sync(&context.canCopy, with: textView.hasSelectedRange) + sync( + &context.canRedoLatestChange, + with: textView.undoManager?.canRedo ?? false) + sync( + &context.canUndoLatestChange, + with: textView.undoManager?.canUndo ?? false) + sync(&context.fontName, with: font.fontName) + sync(&context.fontSize, with: font.pointSize) + sync(&context.isEditingText, with: textView.isFirstResponder) + sync( + &context.paragraphStyle, + with: textView.richTextParagraphStyle ?? .default) + sync( + &context.textAlignment, + with: textView.richTextAlignment ?? .left) + sync(&context.link, with: textView.richTextLink) + + RichTextColor.allCases.forEach { + if let color = textView.richTextColor($0) { + context.setColor($0, to: color) } + } - /// Sync the text binding with the text view. - func syncTextWithTextView() { - DispatchQueue.main.async { - self.text.wrappedValue = self.textView.attributedString - } - } + let styles = textView.richTextStyles + RichTextStyle.allCases.forEach { + let style = styles.hasStyle($0) + context.setStyleInternal($0, to: style) + } - /** + updateTextViewAttributesIfNeeded() + } + + /// Sync the text binding with the text view. + func syncTextWithTextView() { + DispatchQueue.main.async { + self.text.wrappedValue = self.textView.attributedString + } + } + + /** On macOS, we have to update the font and colors when we move the text input cursor and there's no selected text. @@ -280,25 +280,25 @@ the presented information will be correct, but when you type, the last selected font, colors etc. will be used. */ - func updateTextViewAttributesIfNeeded() { - #if macOS - if textView.hasSelectedRange { return } - let attributes = textView.richTextAttributes - textView.setRichTextAttributes(attributes) - #endif - } + func updateTextViewAttributesIfNeeded() { + #if os(macOS) + if textView.hasSelectedRange { return } + let attributes = textView.richTextAttributes + textView.setRichTextAttributes(attributes) + #endif + } - /** + /** On macOS, we have to update the typingAttributes when we move the text input cursor and there's no selected text. So that the current attributes will set again for updated location. */ - func replaceCurrentAttributesIfNeeded() { - #if macOS - if textView.hasSelectedRange { return } - let attributes = textView.richTextAttributes - textView.setNewRichTextAttributes(attributes) - #endif - } + func replaceCurrentAttributesIfNeeded() { + #if os(macOS) + if textView.hasSelectedRange { return } + let attributes = textView.richTextAttributes + textView.setNewRichTextAttributes(attributes) + #endif } + } #endif diff --git a/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift b/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift index e517ee1..df195a6 100644 --- a/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift +++ b/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift @@ -5,27 +5,27 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if macOS - import AppKit +#if os(macOS) + import AppKit - /// This typealias bridges platform-specific colors to simplify - /// multi-platform support. - public typealias ColorRepresentable = NSColor + /// This typealias bridges platform-specific colors to simplify + /// multi-platform support. + public typealias ColorRepresentable = NSColor #endif #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) - import UIKit + import UIKit - /// This typealias bridges platform-specific colors to simplify - /// multi-platform support. - public typealias ColorRepresentable = UIColor + /// This typealias bridges platform-specific colors to simplify + /// multi-platform support. + public typealias ColorRepresentable = UIColor #endif #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) - extension ColorRepresentable { + extension ColorRepresentable { - #if os(iOS) || os(tvOS) || os(visionOS) - public static var textColor: ColorRepresentable { .label } - #endif - } + #if os(iOS) || os(tvOS) || os(visionOS) + public static var textColor: ColorRepresentable { .label } + #endif + } #endif diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift index 3c3f119..f27cff5 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift @@ -9,72 +9,72 @@ import Foundation extension RichTextViewComponent { - /// Get all attributes. - public var richTextAttributes: RichTextAttributes { - if hasSelectedRange { - return richTextAttributes(at: selectedRange) - } - - #if macOS - let range = NSRange(location: selectedRange.location - 1, length: 1) - let safeRange = safeRange(for: range) - return richTextAttributes(at: safeRange) - #else - return typingAttributes - #endif + /// Get all attributes. + public var richTextAttributes: RichTextAttributes { + if hasSelectedRange { + return richTextAttributes(at: selectedRange) } - /// Get a certain attribute. - public func richTextAttribute( - _ attribute: RichTextAttribute - ) -> Value? { - richTextAttributes[attribute] as? Value - } + #if os(macOS) + let range = NSRange(location: selectedRange.location - 1, length: 1) + let safeRange = safeRange(for: range) + return richTextAttributes(at: safeRange) + #else + return typingAttributes + #endif + } - /// Set a certain attribute. - public func setRichTextAttribute( - _ attribute: RichTextAttribute, - to value: Any - ) { - if hasSelectedRange { - setRichTextAttribute(attribute, to: value, at: selectedRange) - } else { - typingAttributes[attribute] = value - } - } + /// Get a certain attribute. + public func richTextAttribute( + _ attribute: RichTextAttribute + ) -> Value? { + richTextAttributes[attribute] as? Value + } - /// Set certain attributes. - public func setRichTextAttributes( - _ attributes: RichTextAttributes - ) { - attributes.forEach { attribute, value in - setRichTextAttribute(attribute, to: value) - } + /// Set a certain attribute. + public func setRichTextAttribute( + _ attribute: RichTextAttribute, + to value: Any + ) { + if hasSelectedRange { + setRichTextAttribute(attribute, to: value, at: selectedRange) + } else { + typingAttributes[attribute] = value } + } - public func setNewRichTextAttributes( - _ attributes: RichTextAttributes - ) { - typingAttributes = attributes + /// Set certain attributes. + public func setRichTextAttributes( + _ attributes: RichTextAttributes + ) { + attributes.forEach { attribute, value in + setRichTextAttribute(attribute, to: value) } + } + + public func setNewRichTextAttributes( + _ attributes: RichTextAttributes + ) { + typingAttributes = attributes + } - /// Remove a certain attribute. - public func removeRichTextAttribute( - _ attribute: RichTextAttribute - ) { - if hasSelectedRange { - removeRichTextAttribute(attribute, at: selectedRange) - } else { - typingAttributes[attribute] = nil - } + /// Remove a certain attribute. + public func removeRichTextAttribute( + _ attribute: RichTextAttribute + ) { + if hasSelectedRange { + removeRichTextAttribute(attribute, at: selectedRange) + } else { + typingAttributes[attribute] = nil } + } - /// Remove certain attributes. - public func removeRichTextAttributes( - _ attributes: RichTextAttributes - ) { - attributes.forEach { attribute, value in - removeRichTextAttribute(attribute) - } + /// Remove certain attributes. + public func removeRichTextAttributes( + _ attributes: RichTextAttributes + ) { + attributes.forEach { attribute, value in + removeRichTextAttribute(attribute) } + } } diff --git a/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift b/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift index 105fc7e..d856055 100644 --- a/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift +++ b/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift @@ -9,126 +9,126 @@ import Foundation extension NSAttributedString { - /** + /** Try to parse ``RichTextDataFormat`` formatted data. - Parameters: - data: The data to initialize the string with. - format: The data format to use. */ - public convenience init( - data: Data, - format: RichTextDataFormat - ) throws { - switch format { - case .archivedData: try self.init(archivedData: data) - case .plainText: try self.init(plainTextData: data) - case .rtf: try self.init(rtfData: data) - case .vendorArchivedData: try self.init(archivedData: data) - } + public convenience init( + data: Data, + format: RichTextDataFormat + ) throws { + switch format { + case .archivedData: try self.init(archivedData: data) + case .plainText: try self.init(plainTextData: data) + case .rtf: try self.init(rtfData: data) + case .vendorArchivedData: try self.init(archivedData: data) } + } } extension NSAttributedString { - /// Try to parse ``RichTextDataFormat/archivedData``. - fileprivate convenience init(archivedData data: Data) throws { - let unarchived = try NSKeyedUnarchiver.unarchivedObject( - ofClass: NSAttributedString.self, - from: data) - guard let string = unarchived else { - throw RichTextDataError.invalidArchivedData(in: data) - } - self.init(attributedString: string) + /// Try to parse ``RichTextDataFormat/archivedData``. + fileprivate convenience init(archivedData data: Data) throws { + let unarchived = try NSKeyedUnarchiver.unarchivedObject( + ofClass: NSAttributedString.self, + from: data) + guard let string = unarchived else { + throw RichTextDataError.invalidArchivedData(in: data) } - - /// Try to parse ``RichTextDataFormat/plainText`` data. - fileprivate convenience init(plainTextData data: Data) throws { - let decoded = String(data: data, encoding: .utf8) - guard let string = decoded else { - throw RichTextDataError.invalidPlainTextData(in: data) - } - let attributed = NSAttributedString(string: string) - self.init(attributedString: attributed) + self.init(attributedString: string) + } + + /// Try to parse ``RichTextDataFormat/plainText`` data. + fileprivate convenience init(plainTextData data: Data) throws { + let decoded = String(data: data, encoding: .utf8) + guard let string = decoded else { + throw RichTextDataError.invalidPlainTextData(in: data) } - - /// Try to parse ``RichTextDataFormat/rtf`` data. - fileprivate convenience init(rtfData data: Data) throws { - var attributes = Self.rtfDataAttributes as NSDictionary? - try self.init( - data: data, - options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes - ) + let attributed = NSAttributedString(string: string) + self.init(attributedString: attributed) + } + + /// Try to parse ``RichTextDataFormat/rtf`` data. + fileprivate convenience init(rtfData data: Data) throws { + var attributes = Self.rtfDataAttributes as NSDictionary? + try self.init( + data: data, + options: [.characterEncoding: Self.utf8], + documentAttributes: &attributes + ) + } + + /// Try to parse ``RichTextDataFormat/rtfd`` data. + fileprivate convenience init(rtfdData data: Data) throws { + var attributes = Self.rtfdDataAttributes as NSDictionary? + try self.init( + data: data, + options: [.characterEncoding: Self.utf8], + documentAttributes: &attributes + ) + } + + #if os(macOS) + /// Try to parse ``RichTextDataFormat/word`` data. + convenience init(wordData data: Data) throws { + var attributes = Self.wordDataAttributes as NSDictionary? + try self.init( + data: data, + options: [.characterEncoding: Self.utf8], + documentAttributes: &attributes + ) } + #endif - /// Try to parse ``RichTextDataFormat/rtfd`` data. - fileprivate convenience init(rtfdData data: Data) throws { - var attributes = Self.rtfdDataAttributes as NSDictionary? - try self.init( - data: data, - options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes - ) + fileprivate convenience init(jsonData data: Data) throws { + let decoder = JSONDecoder() + let richText = try? decoder.decode(RichText.self, from: data) + guard let richText = richText else { + throw RichTextDataError.invalidPlainTextData(in: data) } - #if macOS - /// Try to parse ``RichTextDataFormat/word`` data. - convenience init(wordData data: Data) throws { - var attributes = Self.wordDataAttributes as NSDictionary? - try self.init( - data: data, - options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes - ) - } - #endif - - fileprivate convenience init(jsonData data: Data) throws { - let decoder = JSONDecoder() - let richText = try? decoder.decode(RichText.self, from: data) - guard let richText = richText else { - throw RichTextDataError.invalidPlainTextData(in: data) - } - - var tempSpans: [RichTextSpanInternal] = [] - var text = "" - richText.spans.forEach({ - let span = RichTextSpanInternal( - from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) - tempSpans.append(span) - text += $0.insert - }) - - let attributedString = NSMutableAttributedString(string: text) - - tempSpans.forEach { span in - attributedString.addAttributes( - span.attributes?.toAttributes() ?? [:], range: span.spanRange) - } - self.init(attributedString: attributedString) + var tempSpans: [RichTextSpanInternal] = [] + var text = "" + richText.spans.forEach({ + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) + tempSpans.append(span) + text += $0.insert + }) + + let attributedString = NSMutableAttributedString(string: text) + + tempSpans.forEach { span in + attributedString.addAttributes( + span.attributes?.toAttributes() ?? [:], range: span.spanRange) } + self.init(attributedString: attributedString) + } } extension NSAttributedString { - fileprivate static var utf8: UInt { - String.Encoding.utf8.rawValue - } + fileprivate static var utf8: UInt { + String.Encoding.utf8.rawValue + } - fileprivate static var rtfDataAttributes: [DocumentAttributeKey: Any] { - [.documentType: NSAttributedString.DocumentType.rtf] - } + fileprivate static var rtfDataAttributes: [DocumentAttributeKey: Any] { + [.documentType: NSAttributedString.DocumentType.rtf] + } - fileprivate static var rtfdDataAttributes: [DocumentAttributeKey: Any] { - [.documentType: NSAttributedString.DocumentType.rtfd] - } + fileprivate static var rtfdDataAttributes: [DocumentAttributeKey: Any] { + [.documentType: NSAttributedString.DocumentType.rtfd] + } - #if macOS - static var wordDataAttributes: [DocumentAttributeKey: Any] { - [.documentType: NSAttributedString.DocumentType.docFormat] - } - #endif + #if os(macOS) + static var wordDataAttributes: [DocumentAttributeKey: Any] { + [.documentType: NSAttributedString.DocumentType.docFormat] + } + #endif } diff --git a/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift b/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift index d4d341c..c94f411 100644 --- a/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift +++ b/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift @@ -18,80 +18,80 @@ extension NSAttributedString: RichTextDataReader {} extension RichTextDataReader { - /** + /** Generate rich text data from the current rich text. - Parameters: - format: The data format to use. */ - public func richTextData( - for format: RichTextDataFormat - ) throws -> Data { - switch format { - case .archivedData: try richTextArchivedData() - case .plainText: try richTextPlainTextData() - case .rtf: try richTextRtfData() - case .vendorArchivedData: try richTextArchivedData() - } + public func richTextData( + for format: RichTextDataFormat + ) throws -> Data { + switch format { + case .archivedData: try richTextArchivedData() + case .plainText: try richTextPlainTextData() + case .rtf: try richTextRtfData() + case .vendorArchivedData: try richTextArchivedData() } + } } extension RichTextDataReader { - /// The full text range. - fileprivate var textRange: NSRange { - NSRange(location: 0, length: richText.length) - } + /// The full text range. + fileprivate var textRange: NSRange { + NSRange(location: 0, length: richText.length) + } - /// The full text range. - fileprivate func documentAttributes( - for documentType: NSAttributedString.DocumentType - ) -> [NSAttributedString.DocumentAttributeKey: Any] { - [.documentType: documentType] - } + /// The full text range. + fileprivate func documentAttributes( + for documentType: NSAttributedString.DocumentType + ) -> [NSAttributedString.DocumentAttributeKey: Any] { + [.documentType: documentType] + } - /// Generate archived formatted data. - fileprivate func richTextArchivedData() throws -> Data { - try NSKeyedArchiver.archivedData( - withRootObject: richText, - requiringSecureCoding: false - ) - } + /// Generate archived formatted data. + fileprivate func richTextArchivedData() throws -> Data { + try NSKeyedArchiver.archivedData( + withRootObject: richText, + requiringSecureCoding: false + ) + } - /// Generate plain text formatted data. - fileprivate func richTextPlainTextData() throws -> Data { - let string = richText.string - guard let data = string.data(using: .utf8) else { - throw - RichTextDataError - .invalidData(in: string) - } - return data + /// Generate plain text formatted data. + fileprivate func richTextPlainTextData() throws -> Data { + let string = richText.string + guard let data = string.data(using: .utf8) else { + throw + RichTextDataError + .invalidData(in: string) } + return data + } - /// Generate RTF formatted data. - fileprivate func richTextRtfData() throws -> Data { - try richText.data( - from: textRange, - documentAttributes: documentAttributes(for: .rtf) - ) - } + /// Generate RTF formatted data. + fileprivate func richTextRtfData() throws -> Data { + try richText.data( + from: textRange, + documentAttributes: documentAttributes(for: .rtf) + ) + } - /// Generate RTFD formatted data. - fileprivate func richTextRtfdData() throws -> Data { - try richText.data( - from: textRange, - documentAttributes: documentAttributes(for: .rtfd) - ) - } + /// Generate RTFD formatted data. + fileprivate func richTextRtfdData() throws -> Data { + try richText.data( + from: textRange, + documentAttributes: documentAttributes(for: .rtfd) + ) + } - #if macOS - /// Generate Word formatted data. - func richTextWordData() throws -> Data { - try richText.data( - from: textRange, - documentAttributes: documentAttributes(for: .docFormat) - ) - } - #endif + #if os(macOS) + /// Generate Word formatted data. + func richTextWordData() throws -> Data { + try richText.data( + from: textRange, + documentAttributes: documentAttributes(for: .docFormat) + ) + } + #endif } diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift index d6ef3fc..6686f1d 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift @@ -9,7 +9,7 @@ import SwiftUI extension RichTextFont { - /** + /** This font picker can be used to pick a font from a list, using ``RichTextFont/PickerFont/all`` as default fonts. @@ -31,88 +31,87 @@ extension RichTextFont { Note that this picker will not apply all configurations. */ - public struct Picker: View { + public struct Picker: View { - /** + /** Create a font picker. - Parameters: - selection: The selected font name. */ - public init( - selection: Binding - ) { - self._selection = selection - self.selectedFont = Config.Font.all.first - } + public init( + selection: Binding + ) { + self._selection = selection + self.selectedFont = Config.Font.all.first + } + + public typealias Config = RichTextFont.PickerConfig + public typealias Font = Config.Font + public typealias FontName = Config.FontName + + @State + private var selectedFont: Font? + + @Binding + private var selection: FontName - public typealias Config = RichTextFont.PickerConfig - public typealias Font = Config.Font - public typealias FontName = Config.FontName - - @State - private var selectedFont: Font? - - @Binding - private var selection: FontName - - @Environment(\.richTextFontPickerConfig) - private var config - - public var body: some View { - SwiftUI.Picker(selection: $selection) { - ForEach(config.fonts) { font in - RichTextFont.PickerItem( - font: font, - fontSize: config.fontSize, - isSelected: false - ) - .tag(font.fontName) - } - } label: { - EmptyView() - } + @Environment(\.richTextFontPickerConfig) + private var config + + public var body: some View { + SwiftUI.Picker(selection: $selection) { + ForEach(config.fonts) { font in + RichTextFont.PickerItem( + font: font, + fontSize: config.fontSize, + isSelected: false + ) + .tag(font.fontName) } + } label: { + EmptyView() + } } + } } extension RichTextFont.PickerFont { - /** + /** A system font has a font name that may be resolved to a different name when picked. We must thus try to pattern match, using the currently selected font name. */ - fileprivate func matches(_ fontName: String) -> Bool { - let compare = fontName.lowercased() - let fontName = self.fontName.lowercased() - if fontName == compare { return true } - if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "")) { - return true - } - if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "-")) - { - return true - } - return false + fileprivate func matches(_ fontName: String) -> Bool { + let compare = fontName.lowercased() + let fontName = self.fontName.lowercased() + if fontName == compare { return true } + if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "")) { + return true } + if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "-")) { + return true + } + return false + } - /** + /** Use the selected font name as tag for the selected font. */ - fileprivate func tag(for selectedFont: Self?, selectedName: String) - -> String - { - let isSelected = fontName == selectedFont?.fontName - return isSelected ? selectedName : fontName - } + fileprivate func tag(for selectedFont: Self?, selectedName: String) + -> String + { + let isSelected = fontName == selectedFont?.fontName + return isSelected ? selectedName : fontName + } } //extension View { // // func withPreviewPickerStyles() -> some View { // NavigationView { -// #if macOS +// #if os(macOS) // Color.clear // #endif // ScrollView { @@ -120,7 +119,7 @@ extension RichTextFont.PickerFont { // self.label("Default") // self.pickerStyle(.automatic).label(".automatic") // self.pickerStyle(.inline).label(".inline") -// #if os(iOS) || macOS +// #if os(iOS) || os(macOS) // self.pickerStyle(.menu).label(".menu") // #endif // #if iOS @@ -128,7 +127,7 @@ extension RichTextFont.PickerFont { // pickerStyle(.navigationLink).label(".navigationLink") // } // #endif -// #if os(iOS) || macOS +// #if os(iOS) || os(macOS) // if #available(iOS 17.0, os(macOS) 14.0, watchOS 10.0, *) { // pickerStyle(.palette).label(".palette") // } diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift index 98219d7..b4e5823 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift @@ -8,16 +8,16 @@ import Foundation #if canImport(AppKit) && !targetEnvironment(macCatalyst) - import AppKit + import AppKit #endif #if canImport(UIKit) - import UIKit + import UIKit #endif extension RichTextFont { - /** + /** This struct defines picker-specific fonts that are used by the various font pickers. @@ -34,125 +34,125 @@ extension RichTextFont { to another value. To edit how fonts are detected by the system, use the ``systemFontNamePrefix``. */ - public struct PickerFont: Identifiable, Equatable { - - public init( - fontName: String - ) { - self.fontName = fontName - self.fontDisplayName = "" - self.fontDisplayName = displayName - } + public struct PickerFont: Identifiable, Equatable { + + public init( + fontName: String + ) { + self.fontName = fontName + self.fontDisplayName = "" + self.fontDisplayName = displayName + } - public let fontName: String - public private(set) var fontDisplayName: String + public let fontName: String + public private(set) var fontDisplayName: String - /// Get the unique font id. - public var id: String { - fontName.lowercased() - } + /// Get the unique font id. + public var id: String { + fontName.lowercased() } + } } // MARK: - Static Properties extension RichTextFont.PickerFont { - /// Get all available system fonts. - public static var all: [Self] { - let all = systemFonts - let system = Self.init( - fontName: Self.systemFontNamePrefix - ) - var sorted = all.sorted { $0.fontDisplayName < $1.fontDisplayName } - sorted.insert(system, at: 0) - return sorted - } - - /// The display name for the standard system font. - public static var standardSystemFontDisplayName: String { - #if macOS - return "Standard" - #else - return "San Francisco" - #endif - } - - /// The font name prefix for the standard system font. - public static var systemFontNamePrefix: String { - #if macOS - return ".AppleSystemUIFont" - #else - return ".SFUI" - #endif - } + /// Get all available system fonts. + public static var all: [Self] { + let all = systemFonts + let system = Self.init( + fontName: Self.systemFontNamePrefix + ) + var sorted = all.sorted { $0.fontDisplayName < $1.fontDisplayName } + sorted.insert(system, at: 0) + return sorted + } + + /// The display name for the standard system font. + public static var standardSystemFontDisplayName: String { + #if os(macOS) + return "Standard" + #else + return "San Francisco" + #endif + } + + /// The font name prefix for the standard system font. + public static var systemFontNamePrefix: String { + #if os(macOS) + return ".AppleSystemUIFont" + #else + return ".SFUI" + #endif + } } // MARK: - Public Properties extension RichTextFont.PickerFont { - /// Get the font display name. - public var displayName: String { - let isSystemFont = isStandardSystemFont - let systemName = Self.standardSystemFontDisplayName - return isSystemFont ? systemName : fontName - } - - /// Check if the a font name represents the system font. - public var isStandardSystemFont: Bool { - let name = fontName.trimmingCharacters(in: .whitespaces) - let prefix = Self.systemFontNamePrefix - return name.hasPrefix(prefix) - } + /// Get the font display name. + public var displayName: String { + let isSystemFont = isStandardSystemFont + let systemName = Self.standardSystemFontDisplayName + return isSystemFont ? systemName : fontName + } + + /// Check if the a font name represents the system font. + public var isStandardSystemFont: Bool { + let name = fontName.trimmingCharacters(in: .whitespaces) + let prefix = Self.systemFontNamePrefix + return name.hasPrefix(prefix) + } } // MARK: - Collection Extensions extension Collection where Element == RichTextFont.PickerFont { - /// Get all available system fonts. - public static var all: [Element] { - Element.all - } + /// Get all available system fonts. + public static var all: [Element] { + Element.all + } - /// Move a certain font topmost in the list. - public func moveTopmost(_ topmost: String) -> [Element] { - let topmost = topmost.trimmingCharacters(in: .whitespaces) - let exists = contains { - $0.fontName.lowercased() == topmost.lowercased() - } - guard exists else { return Array(self) } - var filtered = filter { - $0.fontName.lowercased() != topmost.lowercased() - } - let new = Element(fontName: topmost) - filtered.insert(new, at: 0) - return filtered + /// Move a certain font topmost in the list. + public func moveTopmost(_ topmost: String) -> [Element] { + let topmost = topmost.trimmingCharacters(in: .whitespaces) + let exists = contains { + $0.fontName.lowercased() == topmost.lowercased() + } + guard exists else { return Array(self) } + var filtered = filter { + $0.fontName.lowercased() != topmost.lowercased() } + let new = Element(fontName: topmost) + filtered.insert(new, at: 0) + return filtered + } } // MARK: - System Fonts extension RichTextFont.PickerFont { - /** + /** Get all available font picker fonts. */ - fileprivate static var systemFonts: [RichTextFont.PickerFont] { - #if canImport(AppKit) && !targetEnvironment(macCatalyst) - return NSFontManager.shared - .availableFontFamilies - .map { - RichTextFont.PickerFont(fontName: $0) - } - #endif - - #if canImport(UIKit) - return UIFont.familyNames - .map { - RichTextFont.PickerFont(fontName: $0) - } - #endif - } + fileprivate static var systemFonts: [RichTextFont.PickerFont] { + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + return NSFontManager.shared + .availableFontFamilies + .map { + RichTextFont.PickerFont(fontName: $0) + } + #endif + + #if canImport(UIKit) + return UIFont.familyNames + .map { + RichTextFont.PickerFont(fontName: $0) + } + #endif + } } diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift index f63ab3a..bbd1842 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift @@ -26,118 +26,118 @@ import Foundation extension RichTextViewComponent { - /// Get the rich text font at current range. - public var richTextFont: FontRepresentable? { - richTextAttributes[.font] as? FontRepresentable ?? typingAttributes[ - .font] as? FontRepresentable - } + /// Get the rich text font at current range. + public var richTextFont: FontRepresentable? { + richTextAttributes[.font] as? FontRepresentable ?? typingAttributes[ + .font] as? FontRepresentable + } - /// Set the rich text font at current range. - public func setRichTextFont(_ font: FontRepresentable) { - setRichTextAttribute(.font, to: font) - } + /// Set the rich text font at current range. + public func setRichTextFont(_ font: FontRepresentable) { + setRichTextAttribute(.font, to: font) + } - /// Set the rich text font name at current range. - public func setRichTextFontName(_ name: String) { - if richTextFont?.fontName == name { return } - if hasSelectedRange { - setFontName(name, at: selectedRange) - } else { - setFontNameAtCurrentPosition(to: name) - } + /// Set the rich text font name at current range. + public func setRichTextFontName(_ name: String) { + if richTextFont?.fontName == name { return } + if hasSelectedRange { + setFontName(name, at: selectedRange) + } else { + setFontNameAtCurrentPosition(to: name) } + } - /// Set the rich text font size at current range. - public func setRichTextFontSize(_ size: CGFloat) { - if size == richTextFont?.pointSize { return } - #if macOS - setFontSize(size, at: selectedRange) - setFontSizeAtCurrentPosition(size) - #else - if hasSelectedRange { - setFontSize(size, at: selectedRange) - } else { - setFontSizeAtCurrentPosition(size) - } - #endif - } + /// Set the rich text font size at current range. + public func setRichTextFontSize(_ size: CGFloat) { + if size == richTextFont?.pointSize { return } + #if os(macOS) + setFontSize(size, at: selectedRange) + setFontSizeAtCurrentPosition(size) + #else + if hasSelectedRange { + setFontSize(size, at: selectedRange) + } else { + setFontSizeAtCurrentPosition(size) + } + #endif + } - /// Step the rich text font size at current range. - public func stepRichTextFontSize(points: Int) { - let old = richTextFont?.pointSize ?? .standardRichTextFontSize - let new = max(0, old + CGFloat(points)) - setRichTextFontSize(new) - } + /// Step the rich text font size at current range. + public func stepRichTextFontSize(points: Int) { + let old = richTextFont?.pointSize ?? .standardRichTextFontSize + let new = max(0, old + CGFloat(points)) + setRichTextFontSize(new) + } } extension RichTextViewComponent { - /// Set the font at the current position. - fileprivate func setFontNameAtCurrentPosition(to name: String) { - var attributes = typingAttributes - let oldFont = - attributes[.font] as? FontRepresentable ?? .standardRichTextFont - let size = oldFont.pointSize - let newFont = FontRepresentable(name: name, size: size) - attributes[.font] = newFont - typingAttributes = attributes - } + /// Set the font at the current position. + fileprivate func setFontNameAtCurrentPosition(to name: String) { + var attributes = typingAttributes + let oldFont = + attributes[.font] as? FontRepresentable ?? .standardRichTextFont + let size = oldFont.pointSize + let newFont = FontRepresentable(name: name, size: size) + attributes[.font] = newFont + typingAttributes = attributes + } - /// Set the font size at the current position. - fileprivate func setFontSizeAtCurrentPosition(_ size: CGFloat) { - var attributes = typingAttributes - let oldFont = - attributes[.font] as? FontRepresentable ?? .standardRichTextFont - let newFont = oldFont.withSize(size) - attributes[.font] = newFont - typingAttributes = attributes - } + /// Set the font size at the current position. + fileprivate func setFontSizeAtCurrentPosition(_ size: CGFloat) { + var attributes = typingAttributes + let oldFont = + attributes[.font] as? FontRepresentable ?? .standardRichTextFont + let newFont = oldFont.withSize(size) + attributes[.font] = newFont + typingAttributes = attributes + } - /// Set the font name at a certain range. - fileprivate func setFontName(_ name: String, at range: NSRange) { - guard let text = mutableRichText else { return } - guard text.length > 0 else { return } - let fontName = settableFontName(for: name) - text.beginEditing() - text.enumerateAttribute(.font, in: range, options: .init()) { - value, range, _ in - let oldFont = value as? FontRepresentable ?? .standardRichTextFont - let size = oldFont.pointSize - let newFont = - FontRepresentable(name: fontName, size: size) - ?? .standardRichTextFont - text.removeAttribute(.font, range: range) - text.addAttribute(.font, value: newFont, range: range) - text.fixAttributes(in: range) - } - text.endEditing() + /// Set the font name at a certain range. + fileprivate func setFontName(_ name: String, at range: NSRange) { + guard let text = mutableRichText else { return } + guard text.length > 0 else { return } + let fontName = settableFontName(for: name) + text.beginEditing() + text.enumerateAttribute(.font, in: range, options: .init()) { + value, range, _ in + let oldFont = value as? FontRepresentable ?? .standardRichTextFont + let size = oldFont.pointSize + let newFont = + FontRepresentable(name: fontName, size: size) + ?? .standardRichTextFont + text.removeAttribute(.font, range: range) + text.addAttribute(.font, value: newFont, range: range) + text.fixAttributes(in: range) } + text.endEditing() + } - /// Set the font size at a certain range. - fileprivate func setFontSize(_ size: CGFloat, at range: NSRange) { - guard let text = mutableRichText else { return } - guard text.length > 0 else { return } - text.beginEditing() - text.enumerateAttribute(.font, in: range, options: .init()) { - value, range, _ in - let oldFont = value as? FontRepresentable ?? .standardRichTextFont - let newFont = oldFont.withSize(size) - text.removeAttribute(.font, range: range) - text.addAttribute(.font, value: newFont, range: range) - text.fixAttributes(in: range) - } - text.endEditing() + /// Set the font size at a certain range. + fileprivate func setFontSize(_ size: CGFloat, at range: NSRange) { + guard let text = mutableRichText else { return } + guard text.length > 0 else { return } + text.beginEditing() + text.enumerateAttribute(.font, in: range, options: .init()) { + value, range, _ in + let oldFont = value as? FontRepresentable ?? .standardRichTextFont + let newFont = oldFont.withSize(size) + text.removeAttribute(.font, range: range) + text.addAttribute(.font, value: newFont, range: range) + text.fixAttributes(in: range) } + text.endEditing() + } } extension RichTextAttributeWriter { - /// We must adjust empty font names on some platforms. - fileprivate func settableFontName(for fontName: String) -> String { - #if macOS - fontName - #else - fontName - #endif - } + /// We must adjust empty font names on some platforms. + fileprivate func settableFontName(for fontName: String) -> String { + #if os(macOS) + fontName + #else + fontName + #endif + } } diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift index 25a23f7..13ace86 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift @@ -6,11 +6,11 @@ // #if os(iOS) || os(macOS) || os(visionOS) - import SwiftUI + import SwiftUI - extension RichTextFormat { + extension RichTextFormat { - /** + /** This horizontal toolbar provides text format controls. This toolbar adapts the layout based on horizontal size @@ -28,116 +28,116 @@ .richTextFormatToolbarConfig(...) ``` */ - public struct Toolbar: RichTextFormatToolbarBase { + public struct Toolbar: RichTextFormatToolbarBase { - /** + /** Create a rich text format sheet. - Parameters: - context: The context to apply changes to. */ - public init( - context: RichEditorState - ) { - self._context = ObservedObject(wrappedValue: context) - } - - @ObservedObject - private var context: RichEditorState - - @Environment(\.richTextFormatToolbarConfig) - var config - - @Environment(\.richTextFormatToolbarStyle) - var style - - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass - - public var body: some View { - VStack(spacing: style.spacing) { - controls - if hasColorPickers { - Divider() - colorPickers(for: context) - } - } - .labelsHidden() - .padding(.vertical, style.padding) - .environment(\.sizeCategory, .medium) - // .background(background) - #if macOS - .frame(minWidth: 650) - #endif - } + public init( + context: RichEditorState + ) { + self._context = ObservedObject(wrappedValue: context) + } + + @ObservedObject + private var context: RichEditorState + + @Environment(\.richTextFormatToolbarConfig) + var config + + @Environment(\.richTextFormatToolbarStyle) + var style + + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass + + public var body: some View { + VStack(spacing: style.spacing) { + controls + if hasColorPickers { + Divider() + colorPickers(for: context) + } } + .labelsHidden() + .padding(.vertical, style.padding) + .environment(\.sizeCategory, .medium) + // .background(background) + #if os(macOS) + .frame(minWidth: 650) + #endif + } } + } - // MARK: - Views + // MARK: - Views - extension RichTextFormat.Toolbar { + extension RichTextFormat.Toolbar { - fileprivate var useSingleLine: Bool { - #if macOS - true - #else - horizontalSizeClass == .regular - #endif - } + fileprivate var useSingleLine: Bool { + #if os(macOS) + true + #else + horizontalSizeClass == .regular + #endif } + } - extension RichTextFormat.Toolbar { + extension RichTextFormat.Toolbar { - fileprivate var background: some View { - Color.clear - .overlay(Color.primary.opacity(0.1)) - .shadow(color: .black.opacity(0.1), radius: 5) - .edgesIgnoringSafeArea(.all) - } + fileprivate var background: some View { + Color.clear + .overlay(Color.primary.opacity(0.1)) + .shadow(color: .black.opacity(0.1), radius: 5) + .edgesIgnoringSafeArea(.all) + } - @ViewBuilder - fileprivate var controls: some View { - if useSingleLine { - HStack { - controlsContent - } - .padding(.horizontal, style.padding) - } else { - VStack(spacing: style.spacing) { - controlsContent - } - .padding(.horizontal, style.padding) - } + @ViewBuilder + fileprivate var controls: some View { + if useSingleLine { + HStack { + controlsContent + } + .padding(.horizontal, style.padding) + } else { + VStack(spacing: style.spacing) { + controlsContent } + .padding(.horizontal, style.padding) + } + } - @ViewBuilder - fileprivate var controlsContent: some View { - HStack { - #if macOS - headerPicker(context: context) - fontPicker(value: $context.fontName) - .onChangeBackPort(of: context.fontName) { newValue in - context.updateStyle(style: .font(newValue)) - } - #endif - styleToggleGroup(for: context) - otherMenuToggleGroup(for: context) - if !useSingleLine { - Spacer() - } - fontSizePicker(for: context) - if horizontalSizeClass == .regular { - Spacer() - } - } - HStack { - #if !macOS - headerPicker(context: context) - #endif - alignmentPicker(context: context) - // superscriptButtons(for: context, greedy: false) - // indentButtons(for: context, greedy: false) + @ViewBuilder + fileprivate var controlsContent: some View { + HStack { + #if os(macOS) + headerPicker(context: context) + fontPicker(value: $context.fontName) + .onChangeBackPort(of: context.fontName) { newValue in + context.updateStyle(style: .font(newValue)) } + #endif + styleToggleGroup(for: context) + otherMenuToggleGroup(for: context) + if !useSingleLine { + Spacer() + } + fontSizePicker(for: context) + if horizontalSizeClass == .regular { + Spacer() } + } + HStack { + #if !macOS + headerPicker(context: context) + #endif + alignmentPicker(context: context) + // superscriptButtons(for: context, greedy: false) + // indentButtons(for: context, greedy: false) + } } + } #endif diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift index 25ab92d..b4ecdb9 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift @@ -6,89 +6,89 @@ // #if os(iOS) || os(macOS) || os(visionOS) - import SwiftUI + import SwiftUI - extension RichTextFormat { + extension RichTextFormat { - /// This type can be used to configure a format toolbar. - public struct ToolbarConfig { + /// This type can be used to configure a format toolbar. + public struct ToolbarConfig { - public init( - headers: [HeaderType] = .all, - alignments: [RichTextAlignment] = .all, - colorPickers: [RichTextColor] = [.foreground], - colorPickersDisclosed: [RichTextColor] = [], - fontPicker: Bool = true, - fontSizePicker: Bool = true, - indentButtons: Bool = true, - lineSpacingPicker: Bool = false, - styles: [RichTextStyle] = .all, - otherMenu: [RichTextOtherMenu] = .all, - superscriptButtons: Bool = true - ) { - self.headers = headers - self.alignments = alignments - self.colorPickers = colorPickers - self.colorPickersDisclosed = colorPickersDisclosed - self.fontPicker = fontPicker - self.fontSizePicker = fontSizePicker - self.indentButtons = indentButtons - self.lineSpacingPicker = lineSpacingPicker - self.styles = styles - self.otherMenu = otherMenu - #if macOS - self.superscriptButtons = superscriptButtons - #else - self.superscriptButtons = false - #endif - } + public init( + headers: [HeaderType] = .all, + alignments: [RichTextAlignment] = .all, + colorPickers: [RichTextColor] = [.foreground], + colorPickersDisclosed: [RichTextColor] = [], + fontPicker: Bool = true, + fontSizePicker: Bool = true, + indentButtons: Bool = true, + lineSpacingPicker: Bool = false, + styles: [RichTextStyle] = .all, + otherMenu: [RichTextOtherMenu] = .all, + superscriptButtons: Bool = true + ) { + self.headers = headers + self.alignments = alignments + self.colorPickers = colorPickers + self.colorPickersDisclosed = colorPickersDisclosed + self.fontPicker = fontPicker + self.fontSizePicker = fontSizePicker + self.indentButtons = indentButtons + self.lineSpacingPicker = lineSpacingPicker + self.styles = styles + self.otherMenu = otherMenu + #if os(macOS) + self.superscriptButtons = superscriptButtons + #else + self.superscriptButtons = false + #endif + } - public var headers: [HeaderType] - public var alignments: [RichTextAlignment] - public var colorPickers: [RichTextColor] - public var colorPickersDisclosed: [RichTextColor] - public var fontPicker: Bool - public var fontSizePicker: Bool - public var indentButtons: Bool - public var lineSpacingPicker: Bool - public var styles: [RichTextStyle] - public var otherMenu: [RichTextOtherMenu] - public var superscriptButtons: Bool - } + public var headers: [HeaderType] + public var alignments: [RichTextAlignment] + public var colorPickers: [RichTextColor] + public var colorPickersDisclosed: [RichTextColor] + public var fontPicker: Bool + public var fontSizePicker: Bool + public var indentButtons: Bool + public var lineSpacingPicker: Bool + public var styles: [RichTextStyle] + public var otherMenu: [RichTextOtherMenu] + public var superscriptButtons: Bool } + } - extension RichTextFormat.ToolbarConfig { + extension RichTextFormat.ToolbarConfig { - /// The standard rich text format toolbar configuration. - public static var standard: Self { .init() } - } + /// The standard rich text format toolbar configuration. + public static var standard: Self { .init() } + } - extension View { + extension View { - /// Apply a rich text format toolbar style. - public func richTextFormatToolbarConfig( - _ value: RichTextFormat.ToolbarConfig - ) -> some View { - self.environment(\.richTextFormatToolbarConfig, value) - } + /// Apply a rich text format toolbar style. + public func richTextFormatToolbarConfig( + _ value: RichTextFormat.ToolbarConfig + ) -> some View { + self.environment(\.richTextFormatToolbarConfig, value) } + } - extension RichTextFormat.ToolbarConfig { + extension RichTextFormat.ToolbarConfig { - fileprivate struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - public static var defaultValue: RichTextFormat.ToolbarConfig { - .init() - } - } + public static var defaultValue: RichTextFormat.ToolbarConfig { + .init() + } } + } - extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a format toolbar config. - public var richTextFormatToolbarConfig: RichTextFormat.ToolbarConfig { - get { self[RichTextFormat.ToolbarConfig.Key.self] } - set { self[RichTextFormat.ToolbarConfig.Key.self] = newValue } - } + /// This value can bind to a format toolbar config. + public var richTextFormatToolbarConfig: RichTextFormat.ToolbarConfig { + get { self[RichTextFormat.ToolbarConfig.Key.self] } + set { self[RichTextFormat.ToolbarConfig.Key.self] = newValue } } + } #endif diff --git a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift index 41a016f..e098c91 100644 --- a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift +++ b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift @@ -7,65 +7,65 @@ // #if os(iOS) || os(macOS) || os(visionOS) - import SwiftUI - - /// This toolbar can be added above an iOS keyboard, to provide - /// rich text formatting in a compact form. - /// - /// This toolbar is needed since the ``RichTextEditor`` can not - /// use a `toolbar` modifier with `.keyboard` placement: - /// - /// ```swift - /// RichTextEditor(text: $text, context: context) - /// .toolbar { - /// ToolbarItemGroup(placement: .keyboard) { - /// .... - /// } - /// } - /// ``` - /// - /// Instead, add this toolbar below a ``RichTextEditor`` to let - /// it automatically show when the text editor is edited in iOS. - /// - /// You can inject additional leading and trailing buttons, and - /// customize the format sheet that is presented when users tap - /// format button: - /// - /// ```swift - /// VStack { - /// RichTextEditor(...) - /// RichTextKeyboardToolbar( - /// context: context, - /// leadingButtons: {}, - /// trailingButtons: {}, - /// formatSheet: { $0 } - /// ) - /// } - /// ``` - /// - /// These view builders provide you with standard views. Return - /// `$0` to use these standard views, or return any custom view - /// that you want to use instead. - /// - /// You can configure and style the view by applying its config - /// and style view modifiers to your view hierarchy: - /// - /// ```swift - /// VStack { - /// RichTextEditor(...) - /// RichTextKeyboardToolbar(...) - /// } - /// .richTextKeyboardToolbarStyle(...) - /// .richTextKeyboardToolbarConfig(...) - /// ``` - /// - /// For more information, see ``RichTextKeyboardToolbarConfig`` - /// and ``RichTextKeyboardToolbarStyle``. - public struct RichTextKeyboardToolbar< - LeadingButtons: View, TrailingButtons: View, FormatSheet: View - >: View { - - /** + import SwiftUI + + /// This toolbar can be added above an iOS keyboard, to provide + /// rich text formatting in a compact form. + /// + /// This toolbar is needed since the ``RichTextEditor`` can not + /// use a `toolbar` modifier with `.keyboard` placement: + /// + /// ```swift + /// RichTextEditor(text: $text, context: context) + /// .toolbar { + /// ToolbarItemGroup(placement: .keyboard) { + /// .... + /// } + /// } + /// ``` + /// + /// Instead, add this toolbar below a ``RichTextEditor`` to let + /// it automatically show when the text editor is edited in iOS. + /// + /// You can inject additional leading and trailing buttons, and + /// customize the format sheet that is presented when users tap + /// format button: + /// + /// ```swift + /// VStack { + /// RichTextEditor(...) + /// RichTextKeyboardToolbar( + /// context: context, + /// leadingButtons: {}, + /// trailingButtons: {}, + /// formatSheet: { $0 } + /// ) + /// } + /// ``` + /// + /// These view builders provide you with standard views. Return + /// `$0` to use these standard views, or return any custom view + /// that you want to use instead. + /// + /// You can configure and style the view by applying its config + /// and style view modifiers to your view hierarchy: + /// + /// ```swift + /// VStack { + /// RichTextEditor(...) + /// RichTextKeyboardToolbar(...) + /// } + /// .richTextKeyboardToolbarStyle(...) + /// .richTextKeyboardToolbarConfig(...) + /// ``` + /// + /// For more information, see ``RichTextKeyboardToolbarConfig`` + /// and ``RichTextKeyboardToolbarStyle``. + public struct RichTextKeyboardToolbar< + LeadingButtons: View, TrailingButtons: View, FormatSheet: View + >: View { + + /** Create a rich text keyboard toolbar. - Parameters: @@ -74,175 +74,174 @@ - trailingButtons: The trailing buttons to place before the trailing actions. - formatSheet: The rich text format sheet to use, by default ``RichTextFormat/Sheet``. */ - public init( - context: RichEditorState, - @ViewBuilder leadingButtons: @escaping (StandardLeadingButtons) -> - LeadingButtons, - @ViewBuilder trailingButtons: @escaping (StandardTrailingButtons) -> - TrailingButtons, - @ViewBuilder formatSheet: @escaping (StandardFormatSheet) -> - FormatSheet - ) { - self._context = ObservedObject(wrappedValue: context) - self.leadingButtons = leadingButtons - self.trailingButtons = trailingButtons - self.formatSheet = formatSheet - } + public init( + context: RichEditorState, + @ViewBuilder leadingButtons: @escaping (StandardLeadingButtons) -> + LeadingButtons, + @ViewBuilder trailingButtons: @escaping (StandardTrailingButtons) -> + TrailingButtons, + @ViewBuilder formatSheet: @escaping (StandardFormatSheet) -> + FormatSheet + ) { + self._context = ObservedObject(wrappedValue: context) + self.leadingButtons = leadingButtons + self.trailingButtons = trailingButtons + self.formatSheet = formatSheet + } + + public typealias StandardLeadingButtons = EmptyView + public typealias StandardTrailingButtons = EmptyView + public typealias StandardFormatSheet = RichTextFormat.Sheet + + private let leadingButtons: (StandardLeadingButtons) -> LeadingButtons + private let trailingButtons: (StandardTrailingButtons) -> TrailingButtons + private let formatSheet: (StandardFormatSheet) -> FormatSheet + + @ObservedObject + private var context: RichEditorState - public typealias StandardLeadingButtons = EmptyView - public typealias StandardTrailingButtons = EmptyView - public typealias StandardFormatSheet = RichTextFormat.Sheet - - private let leadingButtons: (StandardLeadingButtons) -> LeadingButtons - private let trailingButtons: - (StandardTrailingButtons) -> TrailingButtons - private let formatSheet: (StandardFormatSheet) -> FormatSheet - - @ObservedObject - private var context: RichEditorState - - @State - private var isFormatSheetPresented = false - - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass - - @Environment(\.richTextKeyboardToolbarConfig) - private var config - - @Environment(\.richTextKeyboardToolbarStyle) - private var style - - public var body: some View { - VStack(spacing: 0) { - HStack(spacing: style.itemSpacing) { - leadingViews - Spacer() - .frame(minWidth: 0, maxWidth: .infinity) - trailingViews - } - .padding(10) - } - .environment(\.sizeCategory, .medium) - .frame(height: style.toolbarHeight) - .overlay(Divider(), alignment: .bottom) - .accentColor(.primary) - .background( - Color.primary.colorInvert() - .overlay(Color.white.opacity(0.2)) - .shadow( - color: style.shadowColor, radius: style.shadowRadius, - x: 0, y: 0) - ) - .opacity(shouldDisplayToolbar ? 1 : 0) - .offset(y: shouldDisplayToolbar ? 0 : style.toolbarHeight) - .frame(height: shouldDisplayToolbar ? nil : 0) - .sheet(isPresented: $isFormatSheetPresented) { - formatSheet( - .init(context: context) - ) - .prefersMediumSize() - } + @State + private var isFormatSheetPresented = false + + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass + + @Environment(\.richTextKeyboardToolbarConfig) + private var config + + @Environment(\.richTextKeyboardToolbarStyle) + private var style + + public var body: some View { + VStack(spacing: 0) { + HStack(spacing: style.itemSpacing) { + leadingViews + Spacer() + .frame(minWidth: 0, maxWidth: .infinity) + trailingViews } + .padding(10) + } + .environment(\.sizeCategory, .medium) + .frame(height: style.toolbarHeight) + .overlay(Divider(), alignment: .bottom) + .accentColor(.primary) + .background( + Color.primary.colorInvert() + .overlay(Color.white.opacity(0.2)) + .shadow( + color: style.shadowColor, radius: style.shadowRadius, + x: 0, y: 0) + ) + .opacity(shouldDisplayToolbar ? 1 : 0) + .offset(y: shouldDisplayToolbar ? 0 : style.toolbarHeight) + .frame(height: shouldDisplayToolbar ? nil : 0) + .sheet(isPresented: $isFormatSheetPresented) { + formatSheet( + .init(context: context) + ) + .prefersMediumSize() + } } - - extension View { - - @ViewBuilder - fileprivate func prefersMediumSize() -> some View { - #if macOS - self - #else - if #available(iOS 16, *) { - self.presentationDetents([.medium]) - } else { - self - } - #endif + } + + extension View { + + @ViewBuilder + fileprivate func prefersMediumSize() -> some View { + #if os(macOS) + self + #else + if #available(iOS 16, *) { + self.presentationDetents([.medium]) + } else { + self } + #endif } + } - extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - fileprivate var isCompact: Bool { - horizontalSizeClass == .compact - } + fileprivate var isCompact: Bool { + horizontalSizeClass == .compact } + } - extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - fileprivate var divider: some View { - Divider() - .frame(height: 25) - } + fileprivate var divider: some View { + Divider() + .frame(height: 25) + } - @ViewBuilder - fileprivate var leadingViews: some View { - RichTextAction.ButtonStack( - context: context, - actions: config.leadingActions, - spacing: style.itemSpacing - ) + @ViewBuilder + fileprivate var leadingViews: some View { + RichTextAction.ButtonStack( + context: context, + actions: config.leadingActions, + spacing: style.itemSpacing + ) - leadingButtons(StandardLeadingButtons()) + leadingButtons(StandardLeadingButtons()) - divider + divider - Button(action: presentFormatSheet) { - Image.richTextFormat - .contentShape(Rectangle()) - } + Button(action: presentFormatSheet) { + Image.richTextFormat + .contentShape(Rectangle()) + } - RichTextStyle.ToggleStack(context: context) - .keyboardShortcutsOnly(if: isCompact) + RichTextStyle.ToggleStack(context: context) + .keyboardShortcutsOnly(if: isCompact) - RichTextFont.SizePickerStack(context: context) - .keyboardShortcutsOnly() - } + RichTextFont.SizePickerStack(context: context) + .keyboardShortcutsOnly() + } - @ViewBuilder - fileprivate var trailingViews: some View { - RichTextAlignment.Picker(selection: $context.textAlignment) - .pickerStyle(.segmented) - .frame(maxWidth: 200) - .keyboardShortcutsOnly(if: isCompact) + @ViewBuilder + fileprivate var trailingViews: some View { + RichTextAlignment.Picker(selection: $context.textAlignment) + .pickerStyle(.segmented) + .frame(maxWidth: 200) + .keyboardShortcutsOnly(if: isCompact) - trailingButtons(StandardTrailingButtons()) + trailingButtons(StandardTrailingButtons()) - RichTextAction.ButtonStack( - context: context, - actions: config.trailingActions, - spacing: style.itemSpacing - ) - } + RichTextAction.ButtonStack( + context: context, + actions: config.trailingActions, + spacing: style.itemSpacing + ) } - - extension View { - - @ViewBuilder - fileprivate func keyboardShortcutsOnly( - if condition: Bool = true - ) -> some View { - if condition { - self.hidden() - .frame(width: 0) - } else { - self - } - } + } + + extension View { + + @ViewBuilder + fileprivate func keyboardShortcutsOnly( + if condition: Bool = true + ) -> some View { + if condition { + self.hidden() + .frame(width: 0) + } else { + self + } } + } - extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - fileprivate var shouldDisplayToolbar: Bool { - context.isEditingText || config.alwaysDisplayToolbar - } + fileprivate var shouldDisplayToolbar: Bool { + context.isEditingText || config.alwaysDisplayToolbar } + } - extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - fileprivate func presentFormatSheet() { - isFormatSheetPresented = true - } + fileprivate func presentFormatSheet() { + isFormatSheetPresented = true } + } #endif diff --git a/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift b/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift index 4d1d8e7..3a37b84 100644 --- a/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift +++ b/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift @@ -19,133 +19,133 @@ extension NSAttributedString: RichTextPdfDataReader {} extension RichTextPdfDataReader { - /** + /** Generate PDF data from the current rich text. This is currently only supported on iOS and macOS. When calling this function on other platforms, it will throw a ``PdfDataError/unsupportedPlatform`` error. */ - public func richTextPdfData(configuration: PdfPageConfiguration = .standard) - throws -> Data + public func richTextPdfData(configuration: PdfPageConfiguration = .standard) + throws -> Data + { + #if os(iOS) || os(visionOS) + try richText.iosPdfData(for: configuration) + #elseif os(macOS) + try richText.macosPdfData(for: configuration) + #else + throw PdfDataError.unsupportedPlatform + #endif + } +} + +#if os(macOS) + import AppKit + + @MainActor + extension NSAttributedString { + + fileprivate func macosPdfData(for configuration: PdfPageConfiguration) + throws -> Data { - #if os(iOS) || os(visionOS) - try richText.iosPdfData(for: configuration) - #elseif macOS - try richText.macosPdfData(for: configuration) - #else - throw PdfDataError.unsupportedPlatform - #endif + do { + let fileUrl = try macosPdfFileUrl() + let printInfo = try macosPdfPrintInfo( + for: configuration, + fileUrl: fileUrl) + + let scrollView = NSTextView.scrollableTextView() + scrollView.frame = configuration.paperRect + let textView = + scrollView.documentView as? NSTextView ?? NSTextView() + sleepToPrepareTextView() + textView.textStorage?.setAttributedString(self) + + let printOperation = NSPrintOperation( + view: textView, printInfo: printInfo) + printOperation.showsPrintPanel = false + printOperation.showsProgressPanel = false + printOperation.run() + + return try Data(contentsOf: fileUrl) + } catch { + throw (error) + } + } + + fileprivate func macosPdfFileUrl() throws -> URL { + let manager = FileManager.default + let cacheUrl = try manager.url( + for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, + create: true) + return + cacheUrl + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("pdf") } -} -#if macOS - import AppKit - - @MainActor - extension NSAttributedString { - - fileprivate func macosPdfData(for configuration: PdfPageConfiguration) - throws -> Data - { - do { - let fileUrl = try macosPdfFileUrl() - let printInfo = try macosPdfPrintInfo( - for: configuration, - fileUrl: fileUrl) - - let scrollView = NSTextView.scrollableTextView() - scrollView.frame = configuration.paperRect - let textView = - scrollView.documentView as? NSTextView ?? NSTextView() - sleepToPrepareTextView() - textView.textStorage?.setAttributedString(self) - - let printOperation = NSPrintOperation( - view: textView, printInfo: printInfo) - printOperation.showsPrintPanel = false - printOperation.showsProgressPanel = false - printOperation.run() - - return try Data(contentsOf: fileUrl) - } catch { - throw (error) - } - } - - fileprivate func macosPdfFileUrl() throws -> URL { - let manager = FileManager.default - let cacheUrl = try manager.url( - for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, - create: true) - return - cacheUrl - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("pdf") - } - - fileprivate func macosPdfPrintInfo( - for configuration: PdfPageConfiguration, - fileUrl: URL - ) throws -> NSPrintInfo { - let printOpts: [NSPrintInfo.AttributeKey: Any] = [ - .jobDisposition: NSPrintInfo.JobDisposition.save, - .jobSavingURL: fileUrl, - ] - let printInfo = NSPrintInfo(dictionary: printOpts) - printInfo.horizontalPagination = .fit - printInfo.verticalPagination = .automatic - printInfo.topMargin = configuration.pageMargins.top - printInfo.leftMargin = configuration.pageMargins.left - printInfo.rightMargin = configuration.pageMargins.right - printInfo.bottomMargin = configuration.pageMargins.bottom - printInfo.isHorizontallyCentered = false - printInfo.isVerticallyCentered = false - return printInfo - } - - fileprivate func sleepToPrepareTextView() { - Thread.sleep(forTimeInterval: 0.1) - } + fileprivate func macosPdfPrintInfo( + for configuration: PdfPageConfiguration, + fileUrl: URL + ) throws -> NSPrintInfo { + let printOpts: [NSPrintInfo.AttributeKey: Any] = [ + .jobDisposition: NSPrintInfo.JobDisposition.save, + .jobSavingURL: fileUrl, + ] + let printInfo = NSPrintInfo(dictionary: printOpts) + printInfo.horizontalPagination = .fit + printInfo.verticalPagination = .automatic + printInfo.topMargin = configuration.pageMargins.top + printInfo.leftMargin = configuration.pageMargins.left + printInfo.rightMargin = configuration.pageMargins.right + printInfo.bottomMargin = configuration.pageMargins.bottom + printInfo.isHorizontallyCentered = false + printInfo.isVerticallyCentered = false + return printInfo } + + fileprivate func sleepToPrepareTextView() { + Thread.sleep(forTimeInterval: 0.1) + } + } #endif #if os(iOS) || os(visionOS) - import UIKit - - @MainActor - extension NSAttributedString { - - fileprivate func iosPdfData(for configuration: PdfPageConfiguration) - throws -> Data - { - let pageRenderer = iosPdfPageRenderer(for: configuration) - let paperRect = configuration.paperRect - let pdfData = NSMutableData() - UIGraphicsBeginPDFContextToData(pdfData, paperRect, nil) - let range = NSRange(location: 0, length: pageRenderer.numberOfPages) - pageRenderer.prepare(forDrawingPages: range) - let bounds = UIGraphicsGetPDFContextBounds() - for i in 0.. UIPrintPageRenderer { - let printFormatter = UISimpleTextPrintFormatter( - attributedText: self) - let paperRect = NSValue(cgRect: configuration.paperRect) - let printableRect = NSValue(cgRect: configuration.printableRect) - let pageRenderer = UIPrintPageRenderer() - pageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0) - pageRenderer.setValue(paperRect, forKey: "paperRect") - pageRenderer.setValue(printableRect, forKey: "printableRect") - return pageRenderer - } + import UIKit + + @MainActor + extension NSAttributedString { + + fileprivate func iosPdfData(for configuration: PdfPageConfiguration) + throws -> Data + { + let pageRenderer = iosPdfPageRenderer(for: configuration) + let paperRect = configuration.paperRect + let pdfData = NSMutableData() + UIGraphicsBeginPDFContextToData(pdfData, paperRect, nil) + let range = NSRange(location: 0, length: pageRenderer.numberOfPages) + pageRenderer.prepare(forDrawingPages: range) + let bounds = UIGraphicsGetPDFContextBounds() + for i in 0.. UIPrintPageRenderer { + let printFormatter = UISimpleTextPrintFormatter( + attributedText: self) + let paperRect = NSValue(cgRect: configuration.paperRect) + let printableRect = NSValue(cgRect: configuration.printableRect) + let pageRenderer = UIPrintPageRenderer() + pageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0) + pageRenderer.setValue(paperRect, forKey: "paperRect") + pageRenderer.setValue(printableRect, forKey: "printableRect") + return pageRenderer } + } #endif diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift index e089b3b..00b8eef 100644 --- a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift @@ -7,7 +7,8 @@ import SwiftUI -extension RichTextOtherMenu { +#if os(iOS) || os(macOS) || os(visionOS) + extension RichTextOtherMenu { /** This button can be used to toggle a ``RichTextOtherMenu``. @@ -17,7 +18,7 @@ extension RichTextOtherMenu { */ public struct Button: View { - /** + /** Create a rich text style button. - Parameters: @@ -25,17 +26,17 @@ extension RichTextOtherMenu { - value: The value to bind to. - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. */ - public init( - style: RichTextOtherMenu, - value: Binding, - fillVertically: Bool = false - ) { - self.style = style - self.value = value - self.fillVertically = fillVertically - } + public init( + style: RichTextOtherMenu, + value: Binding, + fillVertically: Bool = false + ) { + self.style = style + self.value = value + self.fillVertically = fillVertically + } - /** + /** Create a rich text style button. - Parameters: @@ -43,44 +44,45 @@ extension RichTextOtherMenu { - context: The context to affect. - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. */ - public init( - style: RichTextOtherMenu, - context: RichEditorState, - fillVertically: Bool = false - ) { - self.init( - style: style, - value: context.binding(for: style), - fillVertically: fillVertically - ) - } + public init( + style: RichTextOtherMenu, + context: RichEditorState, + fillVertically: Bool = false + ) { + self.init( + style: style, + value: context.bindingForManu(for: style), + fillVertically: fillVertically + ) + } - private let style: RichTextOtherMenu - private let value: Binding - private let fillVertically: Bool + private let style: RichTextOtherMenu + private let value: Binding + private let fillVertically: Bool - public var body: some View { - SwiftUI.Button(action: toggle) { - style.label - .labelStyle(.iconOnly) - .frame(maxHeight: fillVertically ? .infinity : nil) - .contentShape(Rectangle()) - } - .tint(.accentColor, if: isOn) - .foreground(.accentColor, if: isOn) - // .keyboardShortcut(for: style) - .accessibilityLabel(style.title) + public var body: some View { + SwiftUI.Button(action: toggle) { + style.label + .labelStyle(.iconOnly) + .frame(maxHeight: fillVertically ? .infinity : nil) + .contentShape(Rectangle()) } + .tint(.accentColor, if: isOn) + .foreground(.accentColor, if: isOn) + // .keyboardShortcut(for: style) + .accessibilityLabel(style.title) + } } -} + } -extension RichTextOtherMenu.Button { + extension RichTextOtherMenu.Button { fileprivate var isOn: Bool { - value.wrappedValue + value.wrappedValue } fileprivate func toggle() { - value.wrappedValue.toggle() + value.wrappedValue.toggle() } -} + } +#endif diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift index 0edf84c..e154323 100644 --- a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift @@ -7,7 +7,8 @@ import SwiftUI -extension RichTextOtherMenu { +#if os(iOS) || os(macOS) || os(visionOS) + extension RichTextOtherMenu { /** This toggle can be used to toggle a ``RichTextOtherMenu``. @@ -18,7 +19,7 @@ extension RichTextOtherMenu { */ public struct Toggle: View { - /** + /** Create a rich text style toggle toggle. - Parameters: @@ -26,17 +27,17 @@ extension RichTextOtherMenu { - value: The value to bind to. - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. */ - public init( - style: RichTextOtherMenu, - value: Binding, - fillVertically: Bool = false - ) { - self.style = style - self.value = value - self.fillVertically = fillVertically - } + public init( + style: RichTextOtherMenu, + value: Binding, + fillVertically: Bool = false + ) { + self.style = style + self.value = value + self.fillVertically = fillVertically + } - /** + /** Create a rich text style toggle. - Parameters: @@ -44,44 +45,45 @@ extension RichTextOtherMenu { - context: The context to affect. - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. */ - public init( - style: RichTextOtherMenu, - context: RichEditorState, - fillVertically: Bool = false - ) { - self.init( - style: style, - value: context.binding(for: style), - fillVertically: fillVertically - ) - } + public init( + style: RichTextOtherMenu, + context: RichEditorState, + fillVertically: Bool = false + ) { + self.init( + style: style, + value: context.bindingForManu(for: style), + fillVertically: fillVertically + ) + } - private let style: RichTextOtherMenu - private let value: Binding - private let fillVertically: Bool + private let style: RichTextOtherMenu + private let value: Binding + private let fillVertically: Bool - public var body: some View { - #if os(tvOS) || os(watchOS) - toggle - #else - toggle.toggleStyle(.button) - #endif - } + public var body: some View { + #if os(tvOS) || os(watchOS) + toggle + #else + toggle.toggleStyle(.button) + #endif + } - private var toggle: some View { - SwiftUI.Toggle(isOn: value) { - style.icon - .frame(maxHeight: fillVertically ? .infinity : nil) - } - // .keyboardShortcut(for: style) - .accessibilityLabel(style.title) + private var toggle: some View { + SwiftUI.Toggle(isOn: value) { + style.icon + .frame(maxHeight: fillVertically ? .infinity : nil) } + // .keyboardShortcut(for: style) + .accessibilityLabel(style.title) + } } -} + } -extension RichTextOtherMenu.Toggle { + extension RichTextOtherMenu.Toggle { fileprivate var isOn: Bool { - value.wrappedValue + value.wrappedValue } -} + } +#endif diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift index a0f2124..13cac44 100644 --- a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift @@ -6,11 +6,11 @@ // #if os(iOS) || os(macOS) || os(visionOS) - import SwiftUI + import SwiftUI - extension RichTextOtherMenu { + extension RichTextOtherMenu { - /** + /** This view can list ``RichTextOtherMenu/Toggle``s for a list of ``RichTextOtherMenu`` values, in a bordered button group. @@ -20,9 +20,9 @@ > Important: Since the `ControlGroup` doesn't highlight buttons in iOS, we use a `ToggleStack` for iOS. */ - public struct ToggleGroup: View { + public struct ToggleGroup: View { - /** + /** Create a rich text style toggle button group. - Parameters: @@ -30,51 +30,51 @@ - styles: The styles to list, by default ``RichTextOtherMenu/all``. - greedy: Whether or not the group is horizontally greedy, by default `true`. */ - public init( - context: RichEditorState, - styles: [RichTextOtherMenu] = .all, - greedy: Bool = true - ) { - self._context = ObservedObject(wrappedValue: context) - self.isGreedy = greedy - self.styles = styles - } + public init( + context: RichEditorState, + styles: [RichTextOtherMenu] = .all, + greedy: Bool = true + ) { + self._context = ObservedObject(wrappedValue: context) + self.isGreedy = greedy + self.styles = styles + } - private let styles: [RichTextOtherMenu] - private let isGreedy: Bool + private let styles: [RichTextOtherMenu] + private let isGreedy: Bool - private var groupWidth: CGFloat? { - if isGreedy { return nil } - let count = Double(styles.count) - #if macOS - return 30 * count - #else - return 50 * count - #endif - } + private var groupWidth: CGFloat? { + if isGreedy { return nil } + let count = Double(styles.count) + #if os(macOS) + return 30 * count + #else + return 50 * count + #endif + } - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - public var body: some View { - #if macOS - ControlGroup { - ForEach(styles) { - RichTextOtherMenu.Toggle( - style: $0, - context: context, - fillVertically: true - ) - } - } - .frame(width: groupWidth) - #else - RichTextOtherMenu.ToggleStack( - context: context, - styles: styles - ) - #endif + public var body: some View { + #if os(macOS) + ControlGroup { + ForEach(styles) { + RichTextOtherMenu.Toggle( + style: $0, + context: context, + fillVertically: true + ) } - } + } + .frame(width: groupWidth) + #else + RichTextOtherMenu.ToggleStack( + context: context, + styles: styles + ) + #endif + } } + } #endif diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift index e4da70d..c205ee3 100644 --- a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift @@ -7,7 +7,9 @@ import SwiftUI -extension RichTextOtherMenu { +#if os(iOS) || os(macOS) || os(visionOS) + + extension RichTextOtherMenu { /** This view can list ``RichTextOtherMenu/Toggle``s for a list @@ -18,7 +20,7 @@ extension RichTextOtherMenu { */ public struct ToggleStack: View { - /** + /** Create a rich text style toggle button group. - Parameters: @@ -26,33 +28,34 @@ extension RichTextOtherMenu { - styles: The styles to list, by default ``RichTextOtherMenu/all``. - spacing: The spacing to apply to stack items, by default `5`. */ - public init( - context: RichEditorState, - styles: [RichTextOtherMenu] = .all, - spacing: Double = 5 - ) { - self._context = ObservedObject(wrappedValue: context) - self.styles = styles - self.spacing = spacing - } - - private let styles: [RichTextOtherMenu] - private let spacing: Double - - @ObservedObject - private var context: RichEditorState - - public var body: some View { - HStack(spacing: spacing) { - ForEach(styles) { - RichTextOtherMenu.Toggle( - style: $0, - context: context, - fillVertically: true - ) - } - } - .fixedSize(horizontal: false, vertical: true) + public init( + context: RichEditorState, + styles: [RichTextOtherMenu] = .all, + spacing: Double = 5 + ) { + self._context = ObservedObject(wrappedValue: context) + self.styles = styles + self.spacing = spacing + } + + private let styles: [RichTextOtherMenu] + private let spacing: Double + + @ObservedObject + private var context: RichEditorState + + public var body: some View { + HStack(spacing: spacing) { + ForEach(styles) { + RichTextOtherMenu.Toggle( + style: $0, + context: context, + fillVertically: true + ) + } } + .fixedSize(horizontal: false, vertical: true) + } } -} + } +#endif diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift index 292c1c7..8da98a2 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift @@ -6,11 +6,11 @@ // #if os(iOS) || os(macOS) || os(visionOS) - import SwiftUI + import SwiftUI - extension RichTextStyle { + extension RichTextStyle { - /** + /** This view can list ``RichTextStyle/Toggle``s for a list of ``RichTextStyle`` values, in a bordered button group. @@ -20,9 +20,9 @@ > Important: Since the `ControlGroup` doesn't highlight buttons in iOS, we use a `ToggleStack` for iOS. */ - public struct ToggleGroup: View { + public struct ToggleGroup: View { - /** + /** Create a rich text style toggle button group. - Parameters: @@ -30,51 +30,51 @@ - styles: The styles to list, by default ``RichTextStyle/all``. - greedy: Whether or not the group is horizontally greedy, by default `true`. */ - public init( - context: RichEditorState, - styles: [RichTextStyle] = .all, - greedy: Bool = true - ) { - self._context = ObservedObject(wrappedValue: context) - self.isGreedy = greedy - self.styles = styles - } + public init( + context: RichEditorState, + styles: [RichTextStyle] = .all, + greedy: Bool = true + ) { + self._context = ObservedObject(wrappedValue: context) + self.isGreedy = greedy + self.styles = styles + } - private let styles: [RichTextStyle] - private let isGreedy: Bool + private let styles: [RichTextStyle] + private let isGreedy: Bool - private var groupWidth: CGFloat? { - if isGreedy { return nil } - let count = Double(styles.count) - #if macOS - return 30 * count - #else - return 50 * count - #endif - } + private var groupWidth: CGFloat? { + if isGreedy { return nil } + let count = Double(styles.count) + #if os(macOS) + return 30 * count + #else + return 50 * count + #endif + } - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - public var body: some View { - #if macOS - ControlGroup { - ForEach(styles) { - RichTextStyle.Toggle( - style: $0, - context: context, - fillVertically: true - ) - } - } - .frame(width: groupWidth) - #else - RichTextStyle.ToggleStack( - context: context, - styles: styles - ) - #endif + public var body: some View { + #if os(macOS) + ControlGroup { + ForEach(styles) { + RichTextStyle.Toggle( + style: $0, + context: context, + fillVertically: true + ) } - } + } + .frame(width: groupWidth) + #else + RichTextStyle.ToggleStack( + context: context, + styles: styles + ) + #endif + } } + } #endif diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift index d1779d0..879efb8 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift @@ -8,57 +8,57 @@ import SwiftUI public enum RichTextStyle: String, CaseIterable, Identifiable, - RichTextLabelValue + RichTextLabelValue { - case bold - case italic - case underline - case strikethrough + case bold + case italic + case underline + case strikethrough } extension RichTextStyle { - /// All available rich text styles. - public static var all: [Self] { allCases } + /// All available rich text styles. + public static var all: [Self] { allCases } } extension Collection where Element == RichTextStyle { - /// All available rich text styles. - public static var all: [RichTextStyle] { RichTextStyle.allCases } + /// All available rich text styles. + public static var all: [RichTextStyle] { RichTextStyle.allCases } } extension RichTextStyle { - public var id: String { rawValue } + public var id: String { rawValue } - /// The standard icon to use for the trait. - public var icon: Image { - switch self { - case .bold: .richTextStyleBold - case .italic: .richTextStyleItalic - case .strikethrough: .richTextStyleStrikethrough - case .underline: .richTextStyleUnderline - } + /// The standard icon to use for the trait. + public var icon: Image { + switch self { + case .bold: .richTextStyleBold + case .italic: .richTextStyleItalic + case .strikethrough: .richTextStyleStrikethrough + case .underline: .richTextStyleUnderline } - - /// The localized style title. - public var title: String { - titleKey.text - } - - /// The localized style title key. - public var titleKey: RTEL10n { - switch self { - case .bold: .styleBold - case .italic: .styleItalic - case .underline: .styleUnderlined - case .strikethrough: .styleStrikethrough - } + } + + /// The localized style title. + public var title: String { + titleKey.text + } + + /// The localized style title key. + public var titleKey: RTEL10n { + switch self { + case .bold: .styleBold + case .italic: .styleItalic + case .underline: .styleUnderlined + case .strikethrough: .styleStrikethrough } + } - /** + /** Get the rich text styles that are enabled in a provided set of traits and attributes. @@ -66,72 +66,72 @@ extension RichTextStyle { - traits: The symbolic traits to inspect. - attributes: The rich text attributes to inspect. */ - public static func styles( - in traits: FontTraitsRepresentable?, - attributes: RichTextAttributes? - ) -> [RichTextStyle] { - var styles = traits?.enabledRichTextStyles ?? [] - if attributes?.isStrikethrough == true { styles.append(.strikethrough) } - if attributes?.isUnderlined == true { styles.append(.underline) } - return styles - } + public static func styles( + in traits: FontTraitsRepresentable?, + attributes: RichTextAttributes? + ) -> [RichTextStyle] { + var styles = traits?.enabledRichTextStyles ?? [] + if attributes?.isStrikethrough == true { styles.append(.strikethrough) } + if attributes?.isUnderlined == true { styles.append(.underline) } + return styles + } } extension Collection where Element == RichTextStyle { - /// Check if the collection contains a certain style. - public func hasStyle(_ style: RichTextStyle) -> Bool { - contains(style) - } - - /// Check if a certain style change should be applied. - public func shouldAddOrRemove( - _ style: RichTextStyle, - _ newValue: Bool - ) -> Bool { - let shouldAdd = newValue && !hasStyle(style) - let shouldRemove = !newValue && hasStyle(style) - return shouldAdd || shouldRemove - } + /// Check if the collection contains a certain style. + public func hasStyle(_ style: RichTextStyle) -> Bool { + contains(style) + } + + /// Check if a certain style change should be applied. + public func shouldAddOrRemove( + _ style: RichTextStyle, + _ newValue: Bool + ) -> Bool { + let shouldAdd = newValue && !hasStyle(style) + let shouldRemove = !newValue && hasStyle(style) + return shouldAdd || shouldRemove + } } #if canImport(UIKit) - extension RichTextStyle { - - /// The symbolic font traits for the style, if any. - public var symbolicTraits: UIFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .traitBold - case .italic: .traitItalic - case .strikethrough: nil - case .underline: nil - } - } + extension RichTextStyle { + + /// The symbolic font traits for the style, if any. + public var symbolicTraits: UIFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .traitBold + case .italic: .traitItalic + case .strikethrough: nil + case .underline: nil + } } + } #endif -#if macOS - extension RichTextStyle { - - /// The symbolic font traits for the trait, if any. - public var symbolicTraits: NSFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .bold - case .italic: .italic - case .strikethrough: nil - case .underline: nil - } - } +#if os(macOS) + extension RichTextStyle { + + /// The symbolic font traits for the trait, if any. + public var symbolicTraits: NSFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .bold + case .italic: .italic + case .strikethrough: nil + case .underline: nil + } } + } #endif extension RichTextStyle { - var richTextSpanStyle: RichTextSpanStyle { - switch self { - case .bold: .bold - case .italic: .italic - case .strikethrough: .strikethrough - case .underline: .underline - } + var richTextSpanStyle: RichTextSpanStyle { + switch self { + case .bold: .bold + case .italic: .italic + case .strikethrough: .strikethrough + case .underline: .underline } + } } diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift index 615e94f..1f109d6 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift @@ -7,50 +7,52 @@ import SwiftUI -extension RichEditorState { +#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) + extension RichEditorState { func insertLink(value: Bool) { - if link != nil { - alertController.showAlert( - title: "Remove link", message: "It will remove link from selected text", - onOk: { [weak self] in - guard let self else { return } - self.updateStyle(style: .link()) - }, - onCancel: { - return - }) - } else { - alertController.showAlert( - title: "Enter url", message: "", placeholder: "Enter link", - defaultText: "", - onTextChange: { text in - }, - completion: { [weak self] finalText in - self?.updateStyle(style: .link(finalText)) - }) - } + if link != nil { + alertController.showAlert( + title: "Remove link", message: "It will remove link from selected text", + onOk: { [weak self] in + guard let self else { return } + self.updateStyle(style: .link()) + }, + onCancel: { + return + }) + } else { + alertController.showAlert( + title: "Enter url", message: "", placeholder: "Enter link", + defaultText: "", + onTextChange: { text in + }, + completion: { [weak self] finalText in + self?.updateStyle(style: .link(finalText)) + }) + } } -} + } -extension RichEditorState { + extension RichEditorState { /// Get a binding for a certain style. - public func binding(for style: RichTextOtherMenu) -> Binding { - Binding( - get: { Bool(self.hasStyle(style)) }, - set: { self.setLink(to: $0) } - ) + public func bindingForManu(for menu: RichTextOtherMenu) -> Binding { + Binding( + get: { Bool(self.hasStyle(menu)) }, + set: { self.setLink(to: $0) } + ) } /// Check whether or not the context has a certain style. public func hasStyle(_ style: RichTextOtherMenu) -> Bool { - link != nil + link != nil } /// Set whether or not the context has a certain style. public func setLink( - to val: Bool + to val: Bool ) { - insertLink(value: val) + insertLink(value: val) } -} + } +#endif diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift index 85c0b46..d08d097 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift @@ -18,12 +18,12 @@ import SwiftUI /// context with focus in a multi-windowed app. public class RichEditorState: ObservableObject { - /// Create a new rich text context instance. - public init() {} + /// Create a new rich text context instance. + public init() {} - // MARK: - Not yet observable properties + // MARK: - Not yet observable properties - /** + /** The currently attributed string, if any. Note that the property is read-only and not `@Published` @@ -36,248 +36,249 @@ public class RichEditorState: ObservableObject { Until then, use `setAttributedString(to:)` to change it. */ - public internal(set) var attributedString = NSAttributedString() + public internal(set) var attributedString = NSAttributedString() - /// The currently selected range, if any. - public internal(set) var selectedRange = NSRange() + /// The currently selected range, if any. + public internal(set) var selectedRange = NSRange() - // MARK: - Bindable & Settable Properies + // MARK: - Bindable & Settable Properies - /// Whether or not the rich text editor is editable. - @Published - public var isEditable = true + /// Whether or not the rich text editor is editable. + @Published + public var isEditable = true - /// Whether or not the text is currently being edited. - @Published - public var isEditingText = false + /// Whether or not the text is currently being edited. + @Published + public var isEditingText = false - @Published - public var headerType: HeaderType = .default + @Published + public var headerType: HeaderType = .default - /// The current text alignment, if any. - @Published - public var textAlignment: RichTextAlignment = .left + /// The current text alignment, if any. + @Published + public var textAlignment: RichTextAlignment = .left - /// The current font name. - @Published - public var fontName = RichTextFont.PickerFont.all.first?.fontName ?? "" + /// The current font name. + @Published + public var fontName = RichTextFont.PickerFont.all.first?.fontName ?? "" - /// The current font size. - @Published - public var fontSize = CGFloat.standardRichTextFontSize + /// The current font size. + @Published + public var fontSize = CGFloat.standardRichTextFontSize - /// The current line spacing. - @Published - public var lineSpacing: CGFloat = 10.0 + /// The current line spacing. + @Published + public var lineSpacing: CGFloat = 10.0 - // MARK: - Observable Properties + // MARK: - Observable Properties - /// Whether or not the current rich text can be copied. - @Published - public internal(set) var canCopy = false + /// Whether or not the current rich text can be copied. + @Published + public internal(set) var canCopy = false - /// Whether or not the latest undo can be redone. - @Published - public internal(set) var canRedoLatestChange = false + /// Whether or not the latest undo can be redone. + @Published + public internal(set) var canRedoLatestChange = false - /// Whether or not the latest change can be undone. - @Published - public internal(set) var canUndoLatestChange = false + /// Whether or not the latest change can be undone. + @Published + public internal(set) var canUndoLatestChange = false - /// The current color values. - @Published - public internal(set) var colors = [RichTextColor: ColorRepresentable]() + /// The current color values. + @Published + public internal(set) var colors = [RichTextColor: ColorRepresentable]() - /// The style to apply when highlighting a range. - @Published - public internal(set) var highlightingStyle = RichTextHighlightingStyle - .standard + /// The style to apply when highlighting a range. + @Published + public internal(set) var highlightingStyle = RichTextHighlightingStyle + .standard - /// The current paragraph style. - @Published - public internal(set) var paragraphStyle = NSParagraphStyle.default + /// The current paragraph style. + @Published + public internal(set) var paragraphStyle = NSParagraphStyle.default - /// The current rich text styles. - @Published - public internal(set) var styles = [RichTextStyle: Bool]() + /// The current rich text styles. + @Published + public internal(set) var styles = [RichTextStyle: Bool]() - @Published - public internal(set) var link: String? = nil + @Published + public internal(set) var link: String? = nil - // MARK: - Properties + // MARK: - Properties - /// This publisher can emit actions to the coordinator. - public let actionPublisher = RichTextAction.Publisher() + /// This publisher can emit actions to the coordinator. + public let actionPublisher = RichTextAction.Publisher() - /// The currently highlighted range, if any. - public var highlightedRange: NSRange? + /// The currently highlighted range, if any. + public var highlightedRange: NSRange? - //MARK: - Variables To Handle JSON - internal var adapter: EditorAdapter = DefaultAdapter() + //MARK: - Variables To Handle JSON + internal var adapter: EditorAdapter = DefaultAdapter() - @Published internal var activeStyles: Set = [] - @Published internal var activeAttributes: [NSAttributedString.Key: Any]? = - [:] + @Published internal var activeStyles: Set = [] + @Published internal var activeAttributes: [NSAttributedString.Key: Any]? = + [:] - internal var internalSpans: [RichTextSpanInternal] = [] + internal var internalSpans: [RichTextSpanInternal] = [] - internal var rawText: String = "" + internal var rawText: String = "" - internal var updateAttributesQueue: - [(span: RichTextSpanInternal, shouldApply: Bool)] = [] - internal let alertController: AlertController = AlertController() + internal var updateAttributesQueue: [(span: RichTextSpanInternal, shouldApply: Bool)] = [] + #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) + internal let alertController: RichTextAlertController = RichTextAlertController() + #endif - /** + /** This will provide encoded text which is of type RichText */ - public var richText: RichText { - return getRichText() - } - - internal var spans: RichTextSpans { - return internalSpans.map({ - .init( - insert: getStringWith(from: $0.from, to: $0.to), - attributes: $0.attributes) - }) - } - - var internalRichText: RichText = .init() - //MARK: - Initializers - /** + public var richText: RichText { + return getRichText() + } + + internal var spans: RichTextSpans { + return internalSpans.map({ + .init( + insert: getStringWith(from: $0.from, to: $0.to), + attributes: $0.attributes) + }) + } + + var internalRichText: RichText = .init() + //MARK: - Initializers + /** Init with richText which is of type RichText */ - public init(richText: RichText) { - internalRichText = richText - let input = richText.spans.map({ $0.insert }).joined() - var tempSpans: [RichTextSpanInternal] = [] - var text = "" - richText.spans.forEach({ - let span = RichTextSpanInternal( - from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) - tempSpans.append(span) - text += $0.insert - }) - - let str = NSMutableAttributedString(string: text) - - tempSpans.forEach { span in - str.addAttributes( - span.attributes?.toAttributes(font: .standardRichTextFont) - ?? [:], range: span.spanRange) - if span.attributes?.color == nil { - var color: ColorRepresentable = .clear - #if os(watchOS) - color = .black - #else - color = RichTextView.Theme.standard.fontColor - #endif - str.addAttributes( - [.foregroundColor: color], range: span.spanRange) - } - } - - self.attributedString = str - - self.internalSpans = tempSpans - - selectedRange = NSRange(location: 0, length: 0) - activeStyles = [] - - rawText = input + public init(richText: RichText) { + internalRichText = richText + let input = richText.spans.map({ $0.insert }).joined() + var tempSpans: [RichTextSpanInternal] = [] + var text = "" + richText.spans.forEach({ + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) + tempSpans.append(span) + text += $0.insert + }) + + let str = NSMutableAttributedString(string: text) + + tempSpans.forEach { span in + str.addAttributes( + span.attributes?.toAttributes(font: .standardRichTextFont) + ?? [:], range: span.spanRange) + if span.attributes?.color == nil { + var color: ColorRepresentable = .clear + #if os(watchOS) + color = .black + #else + color = RichTextView.Theme.standard.fontColor + #endif + str.addAttributes( + [.foregroundColor: color], range: span.spanRange) + } } - /** + self.attributedString = str + + self.internalSpans = tempSpans + + selectedRange = NSRange(location: 0, length: 0) + activeStyles = [] + + rawText = input + } + + /** Init with input which is of type String */ - public init(input: String) { - let adapter = DefaultAdapter() + public init(input: String) { + let adapter = DefaultAdapter() - self.adapter = adapter + self.adapter = adapter - let str = NSMutableAttributedString(string: input) + let str = NSMutableAttributedString(string: input) - str.addAttributes( - [.font: FontRepresentable.standardRichTextFont], - range: str.richTextRange) - self.attributedString = str + str.addAttributes( + [.font: FontRepresentable.standardRichTextFont], + range: str.richTextRange) + self.attributedString = str - self.internalSpans = [ - .init( - from: 0, to: input.utf16Length > 0 ? input.utf16Length - 1 : 0, - attributes: RichAttributes()) - ] + self.internalSpans = [ + .init( + from: 0, to: input.utf16Length > 0 ? input.utf16Length - 1 : 0, + attributes: RichAttributes()) + ] - selectedRange = NSRange(location: 0, length: 0) - activeStyles = [] + selectedRange = NSRange(location: 0, length: 0) + activeStyles = [] - rawText = input - } + rawText = input + } } extension RichEditorState { - /// Whether or not the context has a selected range. - public var hasHighlightedRange: Bool { - highlightedRange != nil - } + /// Whether or not the context has a selected range. + public var hasHighlightedRange: Bool { + highlightedRange != nil + } - /// Whether or not the context has a selected range. - public var hasSelectedRange: Bool { - selectedRange.length > 0 - } + /// Whether or not the context has a selected range. + public var hasSelectedRange: Bool { + selectedRange.length > 0 + } } extension RichEditorState { - /// Set ``highlightedRange`` to a new, optional range. - public func highlightRange(_ range: NSRange?) { - actionPublisher.send(.setHighlightedRange(range)) - highlightedRange = range - } - - /// Reset the attributed string. - public func resetAttributedString() { - setAttributedString(to: "") - } - - /// Reset the ``highlightedRange``. - public func resetHighlightedRange() { - guard hasHighlightedRange else { return } - highlightedRange = nil - } - - /// Reset the ``selectedRange``. - public func resetSelectedRange() { - selectedRange = NSRange() - } - - /// Set a new range and start editing. - public func selectRange(_ range: NSRange) { - isEditingText = true - actionPublisher.send(.selectRange(range)) - } - - /// Set the attributed string to a new plain text. - public func setAttributedString(to text: String) { - setAttributedString(to: NSAttributedString(string: text)) - } - - /// Set the attributed string to a new rich text. - public func setAttributedString(to string: NSAttributedString) { - let mutable = NSMutableAttributedString(attributedString: string) - actionPublisher.send(.setAttributedString(mutable)) - } - - /// Set ``isEditingText`` to `false`. - public func stopEditingText() { - isEditingText = false - } - - /// Toggle whether or not the text is being edited. - public func toggleIsEditing() { - isEditingText.toggle() - } + /// Set ``highlightedRange`` to a new, optional range. + public func highlightRange(_ range: NSRange?) { + actionPublisher.send(.setHighlightedRange(range)) + highlightedRange = range + } + + /// Reset the attributed string. + public func resetAttributedString() { + setAttributedString(to: "") + } + + /// Reset the ``highlightedRange``. + public func resetHighlightedRange() { + guard hasHighlightedRange else { return } + highlightedRange = nil + } + + /// Reset the ``selectedRange``. + public func resetSelectedRange() { + selectedRange = NSRange() + } + + /// Set a new range and start editing. + public func selectRange(_ range: NSRange) { + isEditingText = true + actionPublisher.send(.selectRange(range)) + } + + /// Set the attributed string to a new plain text. + public func setAttributedString(to text: String) { + setAttributedString(to: NSAttributedString(string: text)) + } + + /// Set the attributed string to a new rich text. + public func setAttributedString(to string: NSAttributedString) { + let mutable = NSMutableAttributedString(attributedString: string) + actionPublisher.send(.setAttributedString(mutable)) + } + + /// Set ``isEditingText`` to `false`. + public func stopEditingText() { + isEditingText = false + } + + /// Toggle whether or not the text is being edited. + public func toggleIsEditing() { + isEditingText.toggle() + } } diff --git a/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift index 22f4c76..76def11 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift @@ -6,163 +6,163 @@ // #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) - import SwiftUI - import Combine - - /// This view can be used to view and edit rich text in SwiftUI. - /// - /// The view uses a platform-specific ``RichTextView`` together - /// with a ``RichEditorState`` and a ``RichTextCoordinator`` to - /// keep the view and context in sync. - /// - /// You can use the provided context to trigger and observe any - /// changes to the text editor. Note that changing the value of - /// the `text` binding will not yet update the editor. Until it - /// is fixed, use `setAttributedString(to:)`. - /// - /// Since the view wraps a native `UIKit` or `AppKit` text view, - /// you can't apply `.toolbar` modifiers to it, like you can do - /// with other SwiftUI views. This means that this doesn't work: - /// - /// ```swift - /// RichTextEditor(text: $text, context: context) - /// .toolbar { - /// ToolbarItemGroup(placement: .keyboard) { - /// .... - /// } - /// } - /// ``` + import SwiftUI + import Combine + + /// This view can be used to view and edit rich text in SwiftUI. + /// + /// The view uses a platform-specific ``RichTextView`` together + /// with a ``RichEditorState`` and a ``RichTextCoordinator`` to + /// keep the view and context in sync. + /// + /// You can use the provided context to trigger and observe any + /// changes to the text editor. Note that changing the value of + /// the `text` binding will not yet update the editor. Until it + /// is fixed, use `setAttributedString(to:)`. + /// + /// Since the view wraps a native `UIKit` or `AppKit` text view, + /// you can't apply `.toolbar` modifiers to it, like you can do + /// with other SwiftUI views. This means that this doesn't work: + /// + /// ```swift + /// RichTextEditor(text: $text, context: context) + /// .toolbar { + /// ToolbarItemGroup(placement: .keyboard) { + /// .... + /// } + /// } + /// ``` + /// + /// This will not show anything. To work around this limitation, + /// use a ``RichTextKeyboardToolbar`` instead. + /// + /// You can configure and style the view by applying its config + /// and style view modifiers to your view hierarchy: + /// + /// ```swift + /// VStack { + /// RichTextEditor(...) + /// ... + /// } + /// .richTextEditorStyle(...) + /// .richTextEditorConfig(...) + /// ``` + /// + /// For more information, see ``RichTextKeyboardToolbarConfig`` + /// and ``RichTextKeyboardToolbarStyle``. + public struct RichTextEditor: ViewRepresentable { + + @State var cancellable: Set = [] + + /// Create a rich text editor with a rich text value and + /// a certain rich text data format. /// - /// This will not show anything. To work around this limitation, - /// use a ``RichTextKeyboardToolbar`` instead. - /// - /// You can configure and style the view by applying its config - /// and style view modifiers to your view hierarchy: - /// - /// ```swift - /// VStack { - /// RichTextEditor(...) - /// ... - /// } - /// .richTextEditorStyle(...) - /// .richTextEditorConfig(...) - /// ``` - /// - /// For more information, see ``RichTextKeyboardToolbarConfig`` - /// and ``RichTextKeyboardToolbarStyle``. - public struct RichTextEditor: ViewRepresentable { - - @State var cancellable: Set = [] - - /// Create a rich text editor with a rich text value and - /// a certain rich text data format. - /// - /// - Parameters: - /// - context: The rich editor state to use. - /// - viewConfiguration: A platform-specific view configuration, if any. - public init( - context: ObservedObject, - format: RichTextDataFormat = .archivedData, - viewConfiguration: @escaping ViewConfiguration = { _ in } - ) { - self._context = context - self.format = format - self.viewConfiguration = viewConfiguration - } - - public typealias ViewConfiguration = (RichTextViewComponent) -> Void - - @ObservedObject - private var context: RichEditorState - - private var viewConfiguration: ViewConfiguration - - private var format: RichTextDataFormat - - @Environment(\.richTextEditorConfig) - private var config - - @Environment(\.richTextEditorStyle) - private var style - - #if os(iOS) || os(tvOS) || os(visionOS) - public let textView = RichTextView() - #endif - - #if macOS - public let scrollView = RichTextView.scrollableTextView() - - public var textView: RichTextView { - scrollView.documentView as? RichTextView ?? RichTextView() - } - #endif - - public func makeCoordinator() -> RichTextCoordinator { - RichTextCoordinator( - text: $context.attributedString, - textView: textView, - richTextContext: context - ) - } - - #if os(iOS) || os(tvOS) || os(visionOS) - public func makeUIView(context: Context) -> some UIView { - textView.setup( - with: self.context.attributedString, format: format) - textView.configuration = config - textView.theme = style - viewConfiguration(textView) - return textView - } - - public func updateUIView(_ view: UIViewType, context: Context) { - // if !(self.context.activeAttributes? - // .contains(where: { $0.key == .font }) ?? false) { - // self.textView.typingAttributes = [.font: style.font] - // } - } - #else - - public func makeNSView(context: Context) -> some NSView { - textView.setup( - with: self.context.attributedString, format: format) - textView.configuration = config - textView.theme = style - viewConfiguration(textView) - return scrollView - } - - public func updateNSView(_ view: NSViewType, context: Context) {} - #endif + /// - Parameters: + /// - context: The rich editor state to use. + /// - viewConfiguration: A platform-specific view configuration, if any. + public init( + context: ObservedObject, + format: RichTextDataFormat = .archivedData, + viewConfiguration: @escaping ViewConfiguration = { _ in } + ) { + self._context = context + self.format = format + self.viewConfiguration = viewConfiguration } - // MARK: RichTextPresenter + public typealias ViewConfiguration = (RichTextViewComponent) -> Void + + @ObservedObject + private var context: RichEditorState - extension RichTextEditor { + private var viewConfiguration: ViewConfiguration + + private var format: RichTextDataFormat + + @Environment(\.richTextEditorConfig) + private var config + + @Environment(\.richTextEditorStyle) + private var style + + #if os(iOS) || os(tvOS) || os(visionOS) + public let textView = RichTextView() + #endif + + #if os(macOS) + public let scrollView = RichTextView.scrollableTextView() + + public var textView: RichTextView { + scrollView.documentView as? RichTextView ?? RichTextView() + } + #endif + + public func makeCoordinator() -> RichTextCoordinator { + RichTextCoordinator( + text: $context.attributedString, + textView: textView, + richTextContext: context + ) + } - /// Get the currently selected range. - public var selectedRange: NSRange { - textView.selectedRange - } + #if os(iOS) || os(tvOS) || os(visionOS) + public func makeUIView(context: Context) -> some UIView { + textView.setup( + with: self.context.attributedString, format: format) + textView.configuration = config + textView.theme = style + viewConfiguration(textView) + return textView + } + + public func updateUIView(_ view: UIViewType, context: Context) { + // if !(self.context.activeAttributes? + // .contains(where: { $0.key == .font }) ?? false) { + // self.textView.typingAttributes = [.font: style.font] + // } + } + #else + + public func makeNSView(context: Context) -> some NSView { + textView.setup( + with: self.context.attributedString, format: format) + textView.configuration = config + textView.theme = style + viewConfiguration(textView) + return scrollView + } + + public func updateNSView(_ view: NSViewType, context: Context) {} + #endif + } + + // MARK: RichTextPresenter + + extension RichTextEditor { + + /// Get the currently selected range. + public var selectedRange: NSRange { + textView.selectedRange } + } - // MARK: RichTextReader + // MARK: RichTextReader - extension RichTextEditor { + extension RichTextEditor { - /// Get the string that is managed by the editor. - public var attributedString: NSAttributedString { - context.attributedString - } + /// Get the string that is managed by the editor. + public var attributedString: NSAttributedString { + context.attributedString } + } - // MARK: RichTextWriter + // MARK: RichTextWriter - extension RichTextEditor { + extension RichTextEditor { - /// Get the mutable string that is managed by the editor. - public var mutableAttributedString: NSMutableAttributedString? { - textView.mutableAttributedString - } + /// Get the mutable string that is managed by the editor. + public var mutableAttributedString: NSMutableAttributedString? { + textView.mutableAttributedString } + } #endif diff --git a/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift index fb44093..17b0aa2 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift @@ -9,440 +9,439 @@ import SwiftUI public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { - public static var allCases: [RichTextSpanStyle] = [ - .default, - .bold, - .italic, - .underline, - .strikethrough, - .h1, - .h2, - .h3, - .h4, - .h5, - .h6, - .bullet(), - .size(), - .font(), - .color(), - .background(), - .align(), - ] - - public func hash(into hasher: inout Hasher) { - hasher.combine(key) - - if case .bullet(let indent) = self { - hasher.combine(indent) - } - if case .align(let alignment) = self { - hasher.combine(alignment) - } + public static var allCases: [RichTextSpanStyle] = [ + .default, + .bold, + .italic, + .underline, + .strikethrough, + .h1, + .h2, + .h3, + .h4, + .h5, + .h6, + .bullet(), + .size(), + .font(), + .color(), + .background(), + .align(), + ] + + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + + if case .bullet(let indent) = self { + hasher.combine(indent) } - - case `default` - case bold - case italic - case underline - case strikethrough - case h1 - case h2 - case h3 - case h4 - case h5 - case h6 - case bullet(_ indent: Int? = nil) - // case ordered(_ indent: Int? = nil) - case size(Int? = nil) - case font(String? = nil) - case color(Color? = nil) - case background(Color? = nil) - case align(RichTextAlignment? = nil) - case link(String? = nil) - - var key: String { - switch self { - case .default: - return "default" - case .bold: - return "bold" - case .italic: - return "italic" - case .underline: - return "underline" - case .strikethrough: - return "strikethrough" - case .h1: - return "h1" - case .h2: - return "h2" - case .h3: - return "h3" - case .h4: - return "h4" - case .h5: - return "h5" - case .h6: - return "h6" - case .bullet: - return "bullet" - // case .ordered: - // return "ordered" - case .size: - return "size" - case .font: - return "font" - case .color: - return "color" - case .background: - return "background" - case .align(let alignment): - return "align" + "\(alignment?.rawValue ?? "")" - case .link(let link): - return "link" + (link ?? "") - } + if case .align(let alignment) = self { + hasher.combine(alignment) } - - func defaultAttributeValue(font: FontRepresentable? = nil) -> Any { - let font = font ?? .systemFont(ofSize: .standardRichTextFontSize) - switch self { - case .underline: - return NSUnderlineStyle.single.rawValue - case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6: - return getFontWithUpdating(font: font) - case .bullet(let indent): - return getListStyleAttributeValue( - listType ?? .bullet(), indent: indent) - case .strikethrough: - return NSUnderlineStyle.single.rawValue - case .size(let size): - if let fontSize = size { - return getFontWithUpdating(font: font) - .withSize(CGFloat(fontSize)) - } else { - return getFontWithUpdating(font: font) - } - case .font(let name): - if let name { - return FontRepresentable( - name: name, - size: .standardRichTextFontSize - ) ?? font - } else { - #if os(watchOS) - return CGFloat.standardRichTextFontSize - #else - return RichTextView.Theme.standard.font - #endif - } - case .color: - #if os(watchOS) - return Color.primary - #else - return RichTextView.Theme.standard.fontColor - #endif - case .background: - return ColorRepresentable.white - case .align: - return RichTextAlignment.left.nativeAlignment - case .link(let link): - return link ?? "" - } + } + + case `default` + case bold + case italic + case underline + case strikethrough + case h1 + case h2 + case h3 + case h4 + case h5 + case h6 + case bullet(_ indent: Int? = nil) + // case ordered(_ indent: Int? = nil) + case size(Int? = nil) + case font(String? = nil) + case color(Color? = nil) + case background(Color? = nil) + case align(RichTextAlignment? = nil) + case link(String? = nil) + + var key: String { + switch self { + case .default: + return "default" + case .bold: + return "bold" + case .italic: + return "italic" + case .underline: + return "underline" + case .strikethrough: + return "strikethrough" + case .h1: + return "h1" + case .h2: + return "h2" + case .h3: + return "h3" + case .h4: + return "h4" + case .h5: + return "h5" + case .h6: + return "h6" + case .bullet: + return "bullet" + // case .ordered: + // return "ordered" + case .size: + return "size" + case .font: + return "font" + case .color: + return "color" + case .background: + return "background" + case .align(let alignment): + return "align" + "\(alignment?.rawValue ?? "")" + case .link(let link): + return "link" + (link ?? "") } - - var attributedStringKey: NSAttributedString.Key { - switch self { - case .underline: - return .underlineStyle - case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6, .size, - .font: - return .font - case .bullet, .align: - return .paragraphStyle - case .strikethrough: - return .strikethroughStyle - case .color: - return .foregroundColor - case .background: - return .backgroundColor - case .link: - return .link - } + } + + func defaultAttributeValue(font: FontRepresentable? = nil) -> Any { + let font = font ?? .systemFont(ofSize: .standardRichTextFontSize) + switch self { + case .underline: + return NSUnderlineStyle.single.rawValue + case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6: + return getFontWithUpdating(font: font) + case .bullet(let indent): + return getListStyleAttributeValue( + listType ?? .bullet(), indent: indent) + case .strikethrough: + return NSUnderlineStyle.single.rawValue + case .size(let size): + if let fontSize = size { + return getFontWithUpdating(font: font) + .withSize(CGFloat(fontSize)) + } else { + return getFontWithUpdating(font: font) + } + case .font(let name): + if let name { + return FontRepresentable( + name: name, + size: .standardRichTextFontSize + ) ?? font + } else { + #if os(watchOS) + return CGFloat.standardRichTextFontSize + #else + return RichTextView.Theme.standard.font + #endif + } + case .color: + #if os(watchOS) + return Color.primary + #else + return RichTextView.Theme.standard.fontColor + #endif + case .background: + return ColorRepresentable.white + case .align: + return RichTextAlignment.left.nativeAlignment + case .link(let link): + return link ?? "" } - - public static func == (lhs: RichTextSpanStyle, rhs: RichTextSpanStyle) - -> Bool - { - return lhs.key == rhs.key + } + + var attributedStringKey: NSAttributedString.Key { + switch self { + case .underline: + return .underlineStyle + case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6, .size, + .font: + return .font + case .bullet, .align: + return .paragraphStyle + case .strikethrough: + return .strikethroughStyle + case .color: + return .foregroundColor + case .background: + return .backgroundColor + case .link: + return .link } - - /// The standard icon to use for the trait. - var icon: Image { - switch self { - case .bold: .richTextStyleBold - case .italic: .richTextStyleItalic - case .strikethrough: .richTextStyleStrikethrough - case .underline: .richTextStyleUnderline - default: .richTextPrint - } + } + + public static func == (lhs: RichTextSpanStyle, rhs: RichTextSpanStyle) + -> Bool + { + return lhs.key == rhs.key + } + + /// The standard icon to use for the trait. + var icon: Image { + switch self { + case .bold: .richTextStyleBold + case .italic: .richTextStyleItalic + case .strikethrough: .richTextStyleStrikethrough + case .underline: .richTextStyleUnderline + default: .richTextPrint } - - /// The localized style title key. - var titleKey: RTEL10n { - switch self { - case .bold: .styleBold - case .italic: .styleItalic - case .underline: .styleUnderlined - case .strikethrough: .styleStrikethrough - default: .done - } + } + + /// The localized style title key. + var titleKey: RTEL10n { + switch self { + case .bold: .styleBold + case .italic: .styleItalic + case .underline: .styleUnderlined + case .strikethrough: .styleStrikethrough + default: .done } - - var richTextStyle: RichTextStyle? { - switch self { - case .bold: .bold - case .italic: .italic - case .underline: .underline - case .strikethrough: .strikethrough - default: nil - } + } + + var richTextStyle: RichTextStyle? { + switch self { + case .bold: .bold + case .italic: .italic + case .underline: .underline + case .strikethrough: .strikethrough + default: nil } - - var listType: ListType? { - switch self { - case .bullet(let indent): - return .bullet(indent) - default: - return nil - } + } + + var listType: ListType? { + switch self { + case .bullet(let indent): + return .bullet(indent) + default: + return nil } - - var headerType: HeaderType { - switch self { - case .h1: - return .h1 - case .h2: - return .h2 - case .h3: - return .h3 - case .h4: - return .h4 - case .h5: - return .h5 - case .h6: - return .h6 - default: - return .default - } - } - - var isHeaderStyle: Bool { - switch self { - case .h1, .h2, .h3, .h4, .h5, .h6: - return true - default: - return false - } + } + + var headerType: HeaderType { + switch self { + case .h1: + return .h1 + case .h2: + return .h2 + case .h3: + return .h3 + case .h4: + return .h4 + case .h5: + return .h5 + case .h6: + return .h6 + default: + return .default } - - var isAlignmentStyle: Bool { - switch self { - case .align: - return true - default: - return false - } + } + + var isHeaderStyle: Bool { + switch self { + case .h1, .h2, .h3, .h4, .h5, .h6: + return true + default: + return false } - - var isList: Bool { - switch self { - case .bullet: - return true - default: - return false - } + } + + var isAlignmentStyle: Bool { + switch self { + case .align: + return true + default: + return false } - - var isDefault: Bool { - switch self { - case .default: - return true - case .align(let alignment): - return alignment == .left - default: - return false - } + } + + var isList: Bool { + switch self { + case .bullet: + return true + default: + return false } - - func getFontWithUpdating(font: FontRepresentable) -> FontRepresentable { - switch self { - case .default: - return font - case .bold, .italic: - return font.addFontStyle(self) - case .underline, .bullet, .strikethrough, .color, .background, .align, - .link: - return font - case .h1: - return font.updateFontSize(multiple: 1.5) - case .h2: - return font.updateFontSize(multiple: 1.4) - case .h3: - return font.updateFontSize(multiple: 1.3) - case .h4: - return font.updateFontSize(multiple: 1.2) - case .h5: - return font.updateFontSize(multiple: 1.1) - case .h6: - return font.updateFontSize(multiple: 1) - case .size(let size): - if let size { - return font.updateFontSize(size: CGFloat(size)) - } else { - return font - } - case .font(let name): - if let name { - return FontRepresentable(name: name, size: font.pointSize) - ?? font - } else { - return font - } - } + } + + var isDefault: Bool { + switch self { + case .default: + return true + case .align(let alignment): + return alignment == .left + default: + return false } - - var fontSizeMultiplier: CGFloat { - switch self { - case .h1: - return 1.5 - case .h2: - return 1.4 - case .h3: - return 1.3 - case .h4: - return 1.2 - case .h5: - return 1.1 - default: - return 1 - } + } + + func getFontWithUpdating(font: FontRepresentable) -> FontRepresentable { + switch self { + case .default: + return font + case .bold, .italic: + return font.addFontStyle(self) + case .underline, .bullet, .strikethrough, .color, .background, .align, + .link: + return font + case .h1: + return font.updateFontSize(multiple: 1.5) + case .h2: + return font.updateFontSize(multiple: 1.4) + case .h3: + return font.updateFontSize(multiple: 1.3) + case .h4: + return font.updateFontSize(multiple: 1.2) + case .h5: + return font.updateFontSize(multiple: 1.1) + case .h6: + return font.updateFontSize(multiple: 1) + case .size(let size): + if let size { + return font.updateFontSize(size: CGFloat(size)) + } else { + return font + } + case .font(let name): + if let name { + return FontRepresentable(name: name, size: font.pointSize) + ?? font + } else { + return font + } } - - func getFontAfterRemovingStyle(font: FontRepresentable) -> FontRepresentable - { - switch self { - case .bold, .italic, .bullet: - return font.removeFontStyle(self) - case .underline, .strikethrough, .color, .background, .align, .link: - return font - case .default, .h1, .h2, .h3, .h4, .h5, .h6, .size, .font: - return font.updateFontSize(size: .standardRichTextFontSize) - } + } + + var fontSizeMultiplier: CGFloat { + switch self { + case .h1: + return 1.5 + case .h2: + return 1.4 + case .h3: + return 1.3 + case .h4: + return 1.2 + case .h5: + return 1.1 + default: + return 1 } - - func getListStyleAttributeValue(_ listType: ListType, indent: Int? = nil) - -> NSMutableParagraphStyle - { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .left - let listItem = TextList( - markerFormat: listType.getMarkerFormat(), options: 0) - paragraphStyle.textLists = Array( - repeating: listItem, count: (indent ?? 0) + 1) - return paragraphStyle + } + + func getFontAfterRemovingStyle(font: FontRepresentable) -> FontRepresentable { + switch self { + case .bold, .italic, .bullet: + return font.removeFontStyle(self) + case .underline, .strikethrough, .color, .background, .align, .link: + return font + case .default, .h1, .h2, .h3, .h4, .h5, .h6, .size, .font: + return font.updateFontSize(size: .standardRichTextFontSize) } + } + + func getListStyleAttributeValue(_ listType: ListType, indent: Int? = nil) + -> NSMutableParagraphStyle + { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + let listItem = TextList( + markerFormat: listType.getMarkerFormat(), options: 0) + paragraphStyle.textLists = Array( + repeating: listItem, count: (indent ?? 0) + 1) + return paragraphStyle + } } #if canImport(UIKit) - extension RichTextSpanStyle { - - /// The symbolic font traits for the style, if any. - public var symbolicTraits: UIFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .traitBold - case .italic: .traitItalic - default: nil - } - } + extension RichTextSpanStyle { + + /// The symbolic font traits for the style, if any. + public var symbolicTraits: UIFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .traitBold + case .italic: .traitItalic + default: nil + } } + } #endif -#if macOS - extension RichTextSpanStyle { - - /// The symbolic font traits for the trait, if any. - public var symbolicTraits: NSFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .bold - case .italic: .italic - default: nil - } - } +#if os(macOS) + extension RichTextSpanStyle { + + /// The symbolic font traits for the trait, if any. + public var symbolicTraits: NSFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .bold + case .italic: .italic + default: nil + } } + } #endif extension RichTextSpanStyle { - func getRichAttribute() -> RichAttributes? { - switch self { - case .default: - return nil - case .bold: - return RichAttributes(bold: true) - case .italic: - return RichAttributes(italic: true) - case .underline: - return RichAttributes(underline: true) - case .strikethrough: - return RichAttributes(strike: true) - case .bullet: - return RichAttributes(list: .bullet()) - case .h1: - return RichAttributes(header: .h1) - case .h2: - return RichAttributes(header: .h2) - case .h3: - return RichAttributes(header: .h3) - case .h4: - return RichAttributes(header: .h4) - case .h5: - return RichAttributes(header: .h5) - case .h6: - return RichAttributes(header: .h6) - case .size(let size): - return RichAttributes(size: size) - case .font(let font): - return RichAttributes(font: font) - case .color(let color): - return RichAttributes(color: color?.hexString) - case .background(let background): - return RichAttributes(background: background?.hexString) - case .align(let alignment): - return RichAttributes(align: alignment) - case .link(let link): - return RichAttributes(link: link) - } + func getRichAttribute() -> RichAttributes? { + switch self { + case .default: + return nil + case .bold: + return RichAttributes(bold: true) + case .italic: + return RichAttributes(italic: true) + case .underline: + return RichAttributes(underline: true) + case .strikethrough: + return RichAttributes(strike: true) + case .bullet: + return RichAttributes(list: .bullet()) + case .h1: + return RichAttributes(header: .h1) + case .h2: + return RichAttributes(header: .h2) + case .h3: + return RichAttributes(header: .h3) + case .h4: + return RichAttributes(header: .h4) + case .h5: + return RichAttributes(header: .h5) + case .h6: + return RichAttributes(header: .h6) + case .size(let size): + return RichAttributes(size: size) + case .font(let font): + return RichAttributes(font: font) + case .color(let color): + return RichAttributes(color: color?.hexString) + case .background(let background): + return RichAttributes(background: background?.hexString) + case .align(let alignment): + return RichAttributes(align: alignment) + case .link(let link): + return RichAttributes(link: link) } + } } extension Collection where Element == RichTextSpanStyle { - /** + /** Check if the collection contains a certain style. - Parameters: - style: The style to look for. */ - public func hasStyle(_ style: RichTextSpanStyle) -> Bool { - contains(style) - } - - /// Check if a certain style change should be applied. - public func shouldAddOrRemove( - _ style: RichTextSpanStyle, - _ newValue: Bool - ) -> Bool { - let shouldAdd = newValue && !hasStyle(style) - let shouldRemove = !newValue && hasStyle(style) - return shouldAdd || shouldRemove - } + public func hasStyle(_ style: RichTextSpanStyle) -> Bool { + contains(style) + } + + /// Check if a certain style change should be applied. + public func shouldAddOrRemove( + _ style: RichTextSpanStyle, + _ newValue: Bool + ) -> Bool { + let shouldAdd = newValue && !hasStyle(style) + let shouldRemove = !newValue && hasStyle(style) + return shouldAdd || shouldRemove + } } diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift index 12c07f8..746c4da 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift @@ -5,34 +5,34 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if macOS - import Foundation +#if os(macOS) + import Foundation - extension RichTextView { + extension RichTextView { - /** + /** This type can be used to configure a ``RichTextEditor``. */ - public struct Configuration { + public struct Configuration { - /// Create a custom configuration - /// - Parameters: - /// - isScrollingEnabled: Whether or not the editor should scroll, by default `true`. - /// - isContinuousSpellCheckingEnabled: Whether the editor spell-checks in realtime. Defaults to `true`. - public init( - isScrollingEnabled: Bool = true, - isContinuousSpellCheckingEnabled: Bool = true - ) { - self.isScrollingEnabled = isScrollingEnabled - self.isContinuousSpellCheckingEnabled = - isContinuousSpellCheckingEnabled - } + /// Create a custom configuration + /// - Parameters: + /// - isScrollingEnabled: Whether or not the editor should scroll, by default `true`. + /// - isContinuousSpellCheckingEnabled: Whether the editor spell-checks in realtime. Defaults to `true`. + public init( + isScrollingEnabled: Bool = true, + isContinuousSpellCheckingEnabled: Bool = true + ) { + self.isScrollingEnabled = isScrollingEnabled + self.isContinuousSpellCheckingEnabled = + isContinuousSpellCheckingEnabled + } - /// Whether or not the editor should scroll. - public var isScrollingEnabled: Bool + /// Whether or not the editor should scroll. + public var isScrollingEnabled: Bool - /// Whether the editor spell-checks in realtime. - public var isContinuousSpellCheckingEnabled: Bool - } + /// Whether the editor spell-checks in realtime. + public var isContinuousSpellCheckingEnabled: Bool } + } #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift index 580b8e2..639f97e 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift @@ -5,16 +5,16 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if macOS - import AppKit +#if os(macOS) + import AppKit - /// This typealias bridges UIKit & AppKit native text views. - public typealias RichTextViewRepresentable = NSTextView + /// This typealias bridges UIKit & AppKit native text views. + public typealias RichTextViewRepresentable = NSTextView #endif #if os(iOS) || os(tvOS) || os(visionOS) - import UIKit + import UIKit - /// This typealias bridges UIKit & AppKit native text views. - public typealias RichTextViewRepresentable = UITextView + /// This typealias bridges UIKit & AppKit native text views. + public typealias RichTextViewRepresentable = UITextView #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift index b12db04..4b5d8a1 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift @@ -5,98 +5,98 @@ // Created by Divyesh Vekariya on 19/10/24. // -#if macOS - import AppKit - - /// This is a platform-agnostic rich text view that can be used - /// in both UIKit and AppKit. - /// - /// The view inherits `NSTextView` in AppKit and `UITextView` - /// in UIKit. It aims to make these views behave more alike and - /// make them implement ``RichTextViewComponent``, which is the - /// protocol that is used within this library. - /// - /// The view will apply a ``RichTextImageConfiguration/disabled`` - /// image config by default. You can change this by setting the - /// property manually or by using a ``RichTextDataFormat`` that - /// supports images. - open class RichTextView: NSTextView, RichTextViewComponent { - - // MARK: - Properties - - /// The configuration to use by the rich text view. - public var configuration: Configuration = .standard - - /// The theme for coloring and setting style to text view. - public var theme: Theme = .standard { - didSet { setup(theme) } - } - - /// The style to use when highlighting text in the view. - public var highlightingStyle: RichTextHighlightingStyle = .standard - - /// The image configuration to use by the rich text view. - // public var imageConfiguration: RichTextImageConfiguration = .disabled - - // MARK: - Overrides - - /// Paste the current pasteboard content into the view. - open override func paste(_ sender: Any?) { - // let pasteboard = NSPasteboard.general - // if let image = pasteboard.image { - // return pasteImage(image, at: selectedRange.location) - // } - super.paste(sender) - } - - /** +#if os(macOS) + import AppKit + + /// This is a platform-agnostic rich text view that can be used + /// in both UIKit and AppKit. + /// + /// The view inherits `NSTextView` in AppKit and `UITextView` + /// in UIKit. It aims to make these views behave more alike and + /// make them implement ``RichTextViewComponent``, which is the + /// protocol that is used within this library. + /// + /// The view will apply a ``RichTextImageConfiguration/disabled`` + /// image config by default. You can change this by setting the + /// property manually or by using a ``RichTextDataFormat`` that + /// supports images. + open class RichTextView: NSTextView, RichTextViewComponent { + + // MARK: - Properties + + /// The configuration to use by the rich text view. + public var configuration: Configuration = .standard + + /// The theme for coloring and setting style to text view. + public var theme: Theme = .standard { + didSet { setup(theme) } + } + + /// The style to use when highlighting text in the view. + public var highlightingStyle: RichTextHighlightingStyle = .standard + + /// The image configuration to use by the rich text view. + // public var imageConfiguration: RichTextImageConfiguration = .disabled + + // MARK: - Overrides + + /// Paste the current pasteboard content into the view. + open override func paste(_ sender: Any?) { + // let pasteboard = NSPasteboard.general + // if let image = pasteboard.image { + // return pasteImage(image, at: selectedRange.location) + // } + super.paste(sender) + } + + /** Try to perform a certain drag operation, which will get and paste images from the drag info into the text. */ - // open override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { - // let pasteboard = draggingInfo.draggingPasteboard - // if let images = pasteboard.images, images.count > 0 { - // pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) - // return true - // } - // // Handle fileURLs that contain known image types - // let images = pasteboard.pasteboardItems?.compactMap { - // if let str = $0.string(forType: NSPasteboard.PasteboardType.fileURL), - // let url = URL(string: str), let image = ImageRepresentable(contentsOf: url) { - // let fileExtension = url.pathExtension.lowercased() - // let imageExtensions = ["jpg", "jpeg", "png", "gif", "tiff", "bmp", "heic"] - // if imageExtensions.contains(fileExtension) { - // return image - // } - // } - // return nil - // } ?? [ImageRepresentable]() - // if images.count > 0 { - // pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) - // return true - // } - // - // return super.performDragOperation(draggingInfo) - // } - - open override func scrollWheel(with event: NSEvent) { - - if configuration.isScrollingEnabled { - return super.scrollWheel(with: event) - } - - // 1st nextResponder is NSClipView - // 2nd nextResponder is NSScrollView - // 3rd nextResponder is NSResponder SwiftUIPlatformViewHost - self.nextResponder? - .nextResponder? - .nextResponder? - .scrollWheel(with: event) - } - - // MARK: - Setup - - /** + // open override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { + // let pasteboard = draggingInfo.draggingPasteboard + // if let images = pasteboard.images, images.count > 0 { + // pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) + // return true + // } + // // Handle fileURLs that contain known image types + // let images = pasteboard.pasteboardItems?.compactMap { + // if let str = $0.string(forType: NSPasteboard.PasteboardType.fileURL), + // let url = URL(string: str), let image = ImageRepresentable(contentsOf: url) { + // let fileExtension = url.pathExtension.lowercased() + // let imageExtensions = ["jpg", "jpeg", "png", "gif", "tiff", "bmp", "heic"] + // if imageExtensions.contains(fileExtension) { + // return image + // } + // } + // return nil + // } ?? [ImageRepresentable]() + // if images.count > 0 { + // pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) + // return true + // } + // + // return super.performDragOperation(draggingInfo) + // } + + open override func scrollWheel(with event: NSEvent) { + + if configuration.isScrollingEnabled { + return super.scrollWheel(with: event) + } + + // 1st nextResponder is NSClipView + // 2nd nextResponder is NSScrollView + // 3rd nextResponder is NSResponder SwiftUIPlatformViewHost + self.nextResponder? + .nextResponder? + .nextResponder? + .scrollWheel(with: event) + } + + // MARK: - Setup + + /** Setup the rich text view with a rich text and a certain ``RichTextDataFormat``. @@ -104,46 +104,46 @@ - text: The text to edit with the text view. - format: The rich text format to edit. */ - open func setup( - with text: NSAttributedString, - format: RichTextDataFormat? - ) { - setupSharedBehavior(with: text, format) - allowsImageEditing = true - allowsUndo = true - layoutManager?.defaultAttachmentScaling = - NSImageScaling.scaleProportionallyDown - isContinuousSpellCheckingEnabled = - configuration.isContinuousSpellCheckingEnabled - setup(theme) - } - - public func setup(with richText: RichText) { - var tempSpans: [RichTextSpanInternal] = [] - var text = "" - richText.spans.forEach({ - let span = RichTextSpanInternal( - from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) - tempSpans.append(span) - text += $0.insert - }) - - let str = NSMutableAttributedString(string: text) - - tempSpans.forEach { span in - str.addAttributes( - span.attributes?.toAttributes(font: .standardRichTextFont) - ?? [:], range: span.spanRange) - } - - setup(with: str, format: .archivedData) - } - - // MARK: - Open Functionality - - /** + open func setup( + with text: NSAttributedString, + format: RichTextDataFormat? + ) { + setupSharedBehavior(with: text, format) + allowsImageEditing = true + allowsUndo = true + layoutManager?.defaultAttachmentScaling = + NSImageScaling.scaleProportionallyDown + isContinuousSpellCheckingEnabled = + configuration.isContinuousSpellCheckingEnabled + setup(theme) + } + + public func setup(with richText: RichText) { + var tempSpans: [RichTextSpanInternal] = [] + var text = "" + richText.spans.forEach({ + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) + tempSpans.append(span) + text += $0.insert + }) + + let str = NSMutableAttributedString(string: text) + + tempSpans.forEach { span in + str.addAttributes( + span.attributes?.toAttributes(font: .standardRichTextFont) + ?? [:], range: span.spanRange) + } + + setup(with: str, format: .archivedData) + } + + // MARK: - Open Functionality + + /** Alert a certain title and message. - Parameters: @@ -151,108 +151,106 @@ - message: The alert message. - buttonTitle: The alert button title. */ - open func alert(title: String, message: String, buttonTitle: String) { - let alert = NSAlert() - alert.messageText = title - alert.informativeText = message - alert.alertStyle = NSAlert.Style.warning - alert.addButton(withTitle: buttonTitle) - alert.runModal() - } - - /// Copy the current selection. - open func copySelection() { - let pasteboard = NSPasteboard.general - let range = safeRange(for: selectedRange) - let text = richText(at: range) - pasteboard.clearContents() - pasteboard.setString(text.string, forType: .string) - } - - /// Try to redo the latest undone change. - open func redoLatestChange() { - undoManager?.redo() - } - - /// Scroll to a certain range. - open func scroll(to range: NSRange) { - scrollRangeToVisible(range) - } - - /// Set the rich text in the text view. - open func setRichText(_ text: NSAttributedString) { - attributedString = text - } - - /// Undo the latest change. - open func undoLatestChange() { - undoManager?.undo() - } + open func alert(title: String, message: String, buttonTitle: String) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = NSAlert.Style.warning + alert.addButton(withTitle: buttonTitle) + alert.runModal() + } + + /// Copy the current selection. + open func copySelection() { + let pasteboard = NSPasteboard.general + let range = safeRange(for: selectedRange) + let text = richText(at: range) + pasteboard.clearContents() + pasteboard.setString(text.string, forType: .string) + } + + /// Try to redo the latest undone change. + open func redoLatestChange() { + undoManager?.redo() + } + + /// Scroll to a certain range. + open func scroll(to range: NSRange) { + scrollRangeToVisible(range) } - // MARK: - Public Extensions + /// Set the rich text in the text view. + open func setRichText(_ text: NSAttributedString) { + attributedString = text + } - extension RichTextView { + /// Undo the latest change. + open func undoLatestChange() { + undoManager?.undo() + } + } - /// The text view's layout manager, if any. - public var layoutManagerWrapper: NSLayoutManager? { - layoutManager - } + // MARK: - Public Extensions - /// The spacing between the text view edges and its text. - public var textContentInset: CGSize { - get { textContainerInset } - set { textContainerInset = newValue } - } + extension RichTextView { - /// The text view's text storage, if any. - public var textStorageWrapper: NSTextStorage? { - textStorage - } + /// The text view's layout manager, if any. + public var layoutManagerWrapper: NSLayoutManager? { + layoutManager } - // MARK: - RichTextProvider + /// The spacing between the text view edges and its text. + public var textContentInset: CGSize { + get { textContainerInset } + set { textContainerInset = newValue } + } - extension RichTextView { + /// The text view's text storage, if any. + public var textStorageWrapper: NSTextStorage? { + textStorage + } + } - /// Get the rich text that is managed by the view. - public var attributedString: NSAttributedString { - get { attributedString() } - set { textStorage?.setAttributedString(newValue) } - } + // MARK: - RichTextProvider + + extension RichTextView { + + /// Get the rich text that is managed by the view. + public var attributedString: NSAttributedString { + get { attributedString() } + set { textStorage?.setAttributedString(newValue) } + } - /// Whether or not the text view is the first responder. - public var isFirstResponder: Bool { - window?.firstResponder == self - } + /// Whether or not the text view is the first responder. + public var isFirstResponder: Bool { + window?.firstResponder == self } + } - // MARK: - RichTextWriter + // MARK: - RichTextWriter - extension RichTextView { + extension RichTextView { - // Get the rich text that is managed by the view. - public var mutableAttributedString: NSMutableAttributedString? { - textStorage - } + // Get the rich text that is managed by the view. + public var mutableAttributedString: NSMutableAttributedString? { + textStorage } + } - // MARK: - Additional Pasteboard Types + // MARK: - Additional Pasteboard Types - extension RichTextView { - public override var readablePasteboardTypes: - [NSPasteboard.PasteboardType] - { - var pasteboardTypes = super.readablePasteboardTypes - pasteboardTypes.append(.png) - return pasteboardTypes - } + extension RichTextView { + public override var readablePasteboardTypes: [NSPasteboard.PasteboardType] { + var pasteboardTypes = super.readablePasteboardTypes + pasteboardTypes.append(.png) + return pasteboardTypes } + } - extension RichTextView { - var textString: String { - return self.string - } + extension RichTextView { + var textString: String { + return self.string } + } #endif diff --git a/Sources/RichEditorSwiftUI/UI/Views/AlertController.swift b/Sources/RichEditorSwiftUI/UI/Views/AlertController.swift deleted file mode 100644 index 21146c5..0000000 --- a/Sources/RichEditorSwiftUI/UI/Views/AlertController.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// AlertController.swift -// RichEditorSwiftUI -// -// Created by Divyesh Vekariya on 19/12/24. -// - -#if canImport(UIKit) - import UIKit - - public class AlertController { - - private var onTextChange: ((String) -> Void)? - - internal var rootController: UIViewController? { - guard - let scene = UIApplication.shared.connectedScenes.first(where: { - $0.activationState == .foregroundActive - }) as? UIWindowScene, - let window = scene.windows.first(where: { $0.isKeyWindow }) - else { - return nil - } - - var root = window.rootViewController - while let presentedViewController = root?.presentedViewController { - root = presentedViewController - } - return root - } - - func showAlert( - title: String, - message: String, - placeholder: String? = nil, - defaultText: String? = nil, - onTextChange: ((String) -> Void)? = nil, - completion: @escaping (String?) -> Void - ) { - // Store the onTextChange closure - self.onTextChange = onTextChange - - // Create the alert controller - let alert = UIAlertController( - title: title, message: message, preferredStyle: .alert) - - // Add a text field - alert.addTextField { textField in - textField.placeholder = placeholder - textField.text = defaultText - - // Add a target to handle text changes - if onTextChange != nil { - textField.addTarget( - self, action: #selector(self.textFieldDidChange(_:)), - for: .editingChanged) - } - } - - alert.addAction( - UIAlertAction( - title: "OK", style: .default, - handler: { [weak alert] _ in - let textFieldText = alert?.textFields?.first?.text - completion(textFieldText) - })) - alert.addAction( - UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - - // Present the alert - rootController?.present(alert, animated: true) - } - - @objc private func textFieldDidChange(_ textField: UITextField) { - onTextChange?(textField.text ?? "") - } - - func showAlert( - title: String, - message: String, - onOk: (() -> Void)? = nil, - onCancel: (() -> Void)? = nil - ) { - // Create the alert controller - let alert = UIAlertController( - title: title, message: message, preferredStyle: .alert) - - alert.addAction( - UIAlertAction( - title: "OK", style: .default, - handler: { _ in - onOk?() - })) - alert.addAction( - UIAlertAction( - title: "Cancel", style: .cancel, - handler: { _ in - onCancel?() - })) - - // Present the alert - rootController?.present(alert, animated: true) - } - } -#endif - -#if canImport(AppKit) - import AppKit - public class AlertController { - - private var onTextChange: ((String) -> Void)? - - // Root Window or ViewController - internal var rootWindow: NSWindow? { - return NSApp.mainWindow - } - - // Show alert with a text field and real-time text change closure - func showAlert( - title: String, - message: String, - placeholder: String? = nil, - defaultText: String? = nil, - onTextChange: ((String) -> Void)? = nil, - completion: @escaping (String?) -> Void - ) { - // Store the onTextChange closure - self.onTextChange = onTextChange - - // Create the alert - let alert = NSAlert() - alert.messageText = title - alert.informativeText = message - alert.alertStyle = .informational - - // Create an input text field - let textField = NSTextField( - frame: NSRect(x: 0, y: 0, width: 200, height: 24)) - textField.placeholderString = placeholder - textField.stringValue = defaultText ?? "" - alert.accessoryView = textField - - // Show real-time text updates - if let onTextChange = onTextChange { - textField.target = self - textField.action = #selector(self.textFieldDidChange(_:)) - } - - // Add the OK and Cancel buttons - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Cancel") - - // Show the alert - let response = alert.runModal() - - // Handle completion based on the response - if response == .alertFirstButtonReturn { - completion(textField.stringValue) - } else { - completion(nil) - } - } - - @objc private func textFieldDidChange(_ textField: NSTextField) { - // Call the closure with the updated text - onTextChange?(textField.stringValue) - } - - // Show a simple alert with OK and Cancel actions - func showAlert( - title: String, - message: String, - onOk: (() -> Void)? = nil, - onCancel: (() -> Void)? = nil - ) { - let alert = NSAlert() - alert.messageText = title - alert.informativeText = message - alert.alertStyle = .informational - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Cancel") - - // Show the alert - let response = alert.runModal() - - // Handle actions based on the response - if response == .alertFirstButtonReturn { - onOk?() - } else { - onCancel?() - } - } - } -#endif diff --git a/Sources/RichEditorSwiftUI/UI/Views/RichTextAlertController.swift b/Sources/RichEditorSwiftUI/UI/Views/RichTextAlertController.swift new file mode 100644 index 0000000..67fdad2 --- /dev/null +++ b/Sources/RichEditorSwiftUI/UI/Views/RichTextAlertController.swift @@ -0,0 +1,211 @@ +// +// RichTextAlertController.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +#if os(macOS) +import AppKit + +/// This typealias bridges platform-specific colors to simplify +/// multi-platform support. +public typealias RichTextAlertController = AlertControllerAppKitImpl +#endif + +#if os(iOS) || os(tvOS) || os(visionOS) +import UIKit + +/// This typealias bridges platform-specific colors to simplify +/// multi-platform support. +public typealias RichTextAlertController = AlertControllerUIKitImpl +#endif + + +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit + + public class AlertControllerUIKitImpl { + + private var onTextChange: ((String) -> Void)? + + internal var rootController: UIViewController? { + guard + let scene = UIApplication.shared.connectedScenes.first(where: { + $0.activationState == .foregroundActive + }) as? UIWindowScene, + let window = scene.windows.first(where: { $0.isKeyWindow }) + else { + return nil + } + + var root = window.rootViewController + while let presentedViewController = root?.presentedViewController { + root = presentedViewController + } + return root + } + + func showAlert( + title: String, + message: String, + placeholder: String? = nil, + defaultText: String? = nil, + onTextChange: ((String) -> Void)? = nil, + completion: @escaping (String?) -> Void + ) { + // Store the onTextChange closure + self.onTextChange = onTextChange + + // Create the alert controller + let alert = UIAlertController( + title: title, message: message, preferredStyle: .alert) + + // Add a text field + alert.addTextField { textField in + textField.placeholder = placeholder + textField.text = defaultText + + // Add a target to handle text changes + if onTextChange != nil { + textField.addTarget( + self, action: #selector(self.textFieldDidChange(_:)), + for: .editingChanged) + } + } + + alert.addAction( + UIAlertAction( + title: "OK", style: .default, + handler: { [weak alert] _ in + let textFieldText = alert?.textFields?.first?.text + completion(textFieldText) + })) + alert.addAction( + UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + // Present the alert + rootController?.present(alert, animated: true) + } + + @objc private func textFieldDidChange(_ textField: UITextField) { + onTextChange?(textField.text ?? "") + } + + func showAlert( + title: String, + message: String, + onOk: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil + ) { + // Create the alert controller + let alert = UIAlertController( + title: title, message: message, preferredStyle: .alert) + + alert.addAction( + UIAlertAction( + title: "OK", style: .default, + handler: { _ in + onOk?() + })) + alert.addAction( + UIAlertAction( + title: "Cancel", style: .cancel, + handler: { _ in + onCancel?() + })) + + // Present the alert + rootController?.present(alert, animated: true) + } + } +#endif + +#if os(macOS) + import AppKit + public class AlertControllerAppKitImpl { + + private var onTextChange: ((String) -> Void)? + + // Root Window or ViewController + internal var rootWindow: NSWindow? { + return NSApp.mainWindow + } + + // Show alert with a text field and real-time text change closure + func showAlert( + title: String, + message: String, + placeholder: String? = nil, + defaultText: String? = nil, + onTextChange: ((String) -> Void)? = nil, + completion: @escaping (String?) -> Void + ) { + // Store the onTextChange closure + self.onTextChange = onTextChange + + // Create the alert + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = .informational + + // Create an input text field + let textField = NSTextField( + frame: NSRect(x: 0, y: 0, width: 200, height: 24)) + textField.placeholderString = placeholder + textField.stringValue = defaultText ?? "" + alert.accessoryView = textField + + // Show real-time text updates + if onTextChange != nil { + textField.target = self + textField.action = #selector(self.textFieldDidChange(_:)) + } + + // Add the OK and Cancel buttons + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + // Show the alert + let response = alert.runModal() + + // Handle completion based on the response + if response == .alertFirstButtonReturn { + completion(textField.stringValue) + } else { + completion(nil) + } + } + + @objc private func textFieldDidChange(_ textField: NSTextField) { + // Call the closure with the updated text + onTextChange?(textField.stringValue) + } + + // Show a simple alert with OK and Cancel actions + func showAlert( + title: String, + message: String, + onOk: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil + ) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = .informational + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + // Show the alert + let response = alert.runModal() + + // Handle actions based on the response + if response == .alertFirstButtonReturn { + onOk?() + } else { + onCancel?() + } + } + } +#endif diff --git a/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift b/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift index 63ea1a7..ff15bec 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift @@ -8,17 +8,17 @@ import SwiftUI #if os(iOS) || os(tvOS) || os(visionOS) - import UIKit + import UIKit - /// This typealias bridges platform-specific view representable - /// types to simplify multi-platform support. - typealias ViewRepresentable = UIViewRepresentable + /// This typealias bridges platform-specific view representable + /// types to simplify multi-platform support. + typealias ViewRepresentable = UIViewRepresentable #endif -#if macOS - import AppKit +#if os(macOS) + import AppKit - /// This typealias bridges platform-specific view representable - /// types to simplify multi-platform support. - typealias ViewRepresentable = NSViewRepresentable + /// This typealias bridges platform-specific view representable + /// types to simplify multi-platform support. + typealias ViewRepresentable = NSViewRepresentable #endif