diff --git a/RichEditorDemo/RichEditorDemo.xcodeproj/project.pbxproj b/RichEditorDemo/RichEditorDemo.xcodeproj/project.pbxproj index 21eeb44..46aec18 100644 --- a/RichEditorDemo/RichEditorDemo.xcodeproj/project.pbxproj +++ b/RichEditorDemo/RichEditorDemo.xcodeproj/project.pbxproj @@ -305,7 +305,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; @@ -344,7 +344,7 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; diff --git a/RichEditorDemo/RichEditorDemo/ContentView.swift b/RichEditorDemo/RichEditorDemo/ContentView.swift index 444dbee..b719bbb 100644 --- a/RichEditorDemo/RichEditorDemo/ContentView.swift +++ b/RichEditorDemo/RichEditorDemo/ContentView.swift @@ -5,20 +5,23 @@ // Created by Divyesh Vekariya on 11/10/23. // -import SwiftUI import RichEditorSwiftUI +import SwiftUI struct ContentView: View { @Environment(\.colorScheme) var colorScheme @ObservedObject var state: RichEditorState + @State private var isInspectorPresented = false init(state: RichEditorState? = nil) { if let state { self.state = state } else { - if let richText = readJSONFromFile(fileName: "Sample_json", - type: RichText.self) { + if let richText = readJSONFromFile( + fileName: "Sample_json", + type: RichText.self) + { self.state = .init(richText: richText) } else { self.state = .init(input: "Hello World!") @@ -29,9 +32,9 @@ struct ContentView: View { var body: some View { NavigationStack { VStack { -#if os(iOS) || os(macOS) || os(visionOS) - EditorToolBarView(state: state) -#endif + #if os(macOS) + RichTextFormat.Toolbar(context: state) + #endif RichTextEditor( context: _state, @@ -40,22 +43,39 @@ struct ContentView: View { } ) .cornerRadius(10) + + #if os(iOS) + RichTextKeyboardToolbar( + context: state, + leadingButtons: { $0 }, + trailingButtons: { $0 }, + formatSheet: { $0 } + ) + #endif + } + .inspector(isPresented: $isInspectorPresented) { + RichTextFormat.Sidebar(context: state) + #if os(macOS) + .inspectorColumnWidth(min: 200, ideal: 200, max: 315) + #endif } .padding(10) .toolbar { - ToolbarItem() { - Button(action: { - print("Exported JSON == \(state.output())") - }, label: { - Image(systemName: "printer.inverse") - .padding() - }) + ToolbarItem(placement: .automatic) { + Button( + action: { + print("Exported JSON == \(state.output())") + }, + label: { + Image(systemName: "printer.inverse") + .padding() + }) } } .background(colorScheme == .dark ? .black : .gray.opacity(0.07)) .navigationTitle("Rich Editor") #if os(iOS) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.inline) #endif } } diff --git a/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift b/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift index 1b1b24b..4bbee89 100644 --- a/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift +++ b/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift @@ -78,7 +78,7 @@ public enum RichTextAction: Identifiable, Equatable { case undoLatestChange /// Set HeaderStyle. - case setHeaderStyle(_ style: RichTextStyle) + case setHeaderStyle(_ style: RichTextSpanStyle) } public extension RichTextAction { diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift index 6e22ff2..d033492 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift @@ -56,12 +56,12 @@ public extension RichTextAttributeReader { public extension RichTextAttributeReader { /// Get the text styles at a certain range. - func richTextStyles(at range: NSRange) -> [RichTextStyle] { + func richTextStyles(at range: NSRange) -> [RichTextSpanStyle] { let attributes = richTextAttributes(at: range) let traits = richTextFont(at: range)?.fontDescriptor.symbolicTraits var styles = traits?.enabledRichTextStyles ?? [] if attributes.isStrikethrough { styles.append(.strikethrough) } if attributes.isUnderlined { styles.append(.underline) } - return styles + return styles.map({ $0.richTextSpanStyle }) } } diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift index 73e186e..027279c 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift @@ -21,7 +21,7 @@ public extension NSMutableAttributedString { - range: The range to affect, by default the entire text. */ func setRichTextStyle( - _ style: RichTextStyle, + _ style: RichTextSpanStyle, to newValue: Bool, at range: NSRange? = nil ) { @@ -43,8 +43,8 @@ public extension NSMutableAttributedString { let shouldRemove = !newValue && styles.hasStyle(style) guard shouldAdd || shouldRemove || style.isHeaderStyle else { return } var descriptor = font.fontDescriptor - if !style.isDefault && !style.isHeaderStyle { - descriptor = descriptor.byTogglingStyle(style) + if let richTextStyle = style.richTextStyle, !style.isDefault && !style.isHeaderStyle { + descriptor = descriptor.byTogglingStyle(richTextStyle) } let newFont: FontRepresentable? = FontRepresentable( descriptor: descriptor, @@ -56,7 +56,7 @@ public extension NSMutableAttributedString { /** This will reset font size before multiplying new size */ - private func byTogglingFontSizeFor(style: TextSpanStyle, font: FontRepresentable, shouldAdd: Bool) -> CGFloat { + 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) diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift index 3fe6919..3cc9da8 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift @@ -183,7 +183,7 @@ extension RichTextCoordinator { func setStyle(_ style: RichTextStyle, to newValue: Bool) { let hasStyle = textView.richTextStyles.hasStyle(style) - if newValue == hasStyle { return } + guard newValue != hasStyle else { return } textView.setRichTextStyle(style, to: newValue) } } diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift index 906ea40..c1b417a 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift @@ -41,8 +41,6 @@ public extension RichTextViewComponent { setRichTextAttribute(.underlineStyle, to: value) case .strikethrough: setRichTextAttribute(.strikethroughStyle, to: value) - default: - return } } diff --git a/Sources/RichEditorSwiftUI/UI/EditorToolBar/HeaderType.swift b/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift similarity index 94% rename from Sources/RichEditorSwiftUI/UI/EditorToolBar/HeaderType.swift rename to Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift index ca11ad2..668c05e 100644 --- a/Sources/RichEditorSwiftUI/UI/EditorToolBar/HeaderType.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift @@ -35,7 +35,7 @@ public enum HeaderType: Int, CaseIterable, Codable { } } - func getTextSpanStyle() -> TextSpanStyle { + func getTextSpanStyle() -> RichTextSpanStyle { switch self { case .default: return .default case .h1: return .h1 diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift index 2c5f59e..a9a3ff6 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift @@ -146,11 +146,11 @@ extension RichAttributes { ) } - public func copy(with style: TextSpanStyle, byAdding: Bool = true) -> RichAttributes { + public func copy(with style: RichTextSpanStyle, byAdding: Bool = true) -> RichAttributes { return copy(with: [style], byAdding: byAdding) } - public func copy(with styles: [TextSpanStyle], byAdding: Bool = true) -> RichAttributes { + public func copy(with styles: [RichTextSpanStyle], byAdding: Bool = true) -> RichAttributes { let att = getRichAttributesFor(styles: styles) return RichAttributes( bold: (att.bold != nil ? (byAdding ? att.bold! : nil) : self.bold), @@ -169,8 +169,8 @@ extension RichAttributes { } extension RichAttributes { - public func styles() -> [TextSpanStyle] { - var styles: [TextSpanStyle] = [] + public func styles() -> [RichTextSpanStyle] { + var styles: [RichTextSpanStyle] = [] if let bold = bold, bold { styles.append(.bold) } @@ -204,8 +204,8 @@ extension RichAttributes { return styles } - public func stylesSet() -> Set { - var styles: Set = [] + public func stylesSet() -> Set { + var styles: Set = [] if let bold = bold, bold { styles.insert(.bold) } @@ -241,7 +241,7 @@ extension RichAttributes { } extension RichAttributes { - public func hasStyle(style: RichTextStyle) -> Bool { + public func hasStyle(style: RichTextSpanStyle) -> Bool { switch style { case .default: return true @@ -279,11 +279,11 @@ extension RichAttributes { } } -internal func getRichAttributesFor(style: RichTextStyle) -> RichAttributes { +internal func getRichAttributesFor(style: RichTextSpanStyle) -> RichAttributes { return getRichAttributesFor(styles: [style]) } -internal func getRichAttributesFor(styles: [RichTextStyle]) -> RichAttributes { +internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttributes { guard !styles.isEmpty else { return RichAttributes() } var bold: Bool? = nil var italic: Bool? = nil diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift index fe88765..9fb4162 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift @@ -59,10 +59,10 @@ public extension RichTextFormat { public var body: some View { NavigationView { VStack(spacing: 0) { -// RichTextFont.ListPicker( -// selection: $context.fontName -// ) -// Divider() + RichTextFont.ListPicker( + selection: $context.fontName + ) + Divider() RichTextFormat.Toolbar( context: context ) diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift index 49b4842..c9cc091 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift @@ -59,9 +59,12 @@ public extension RichTextFormat { VStack(alignment: .leading, spacing: style.spacing) { SidebarSection { fontPicker(value: $context.fontName) + .onChangeBackPort(of: context.fontName) { newValue in + context.updateStyle(style: .font(newValue)) + } HStack { -// styleToggleGroup(for: context) -// Spacer() + styleToggleGroup(for: context) + Spacer() fontSizePicker(for: context) } } diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift index d9c3d69..53a274d 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift @@ -56,9 +56,9 @@ public extension RichTextFormat { public var body: some View { VStack(spacing: style.spacing) { -// controls + controls if hasColorPickers { -// Divider() + Divider() colorPickers(for: context) } } @@ -115,8 +115,11 @@ private extension RichTextFormat.Toolbar { HStack { #if macOS fontPicker(value: $context.fontName) + .onChangeBackPort(of: context.fontName) { newValue in + context.updateStyle(style: .font(newValue)) + } #endif -// styleToggleGroup(for: context) + styleToggleGroup(for: context) if !useSingleLine { Spacer() } diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift index 0fdf072..52f6e0f 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift @@ -88,6 +88,7 @@ extension RichTextFormatToolbarBase { .labelStyle(.iconOnly) .frame(minWidth: 30) } + .padding(.horizontal) } } @@ -148,17 +149,17 @@ extension RichTextFormatToolbarBase { // } // } -// @ViewBuilder -// func styleToggleGroup( -// for context: RichEditorState -// ) -> some View { -// if !config.styles.isEmpty { -// RichTextStyle.ToggleGroup( -// context: context, -// styles: config.styles -// ) -// } -// } + @ViewBuilder + func styleToggleGroup( + for context: RichEditorState + ) -> some View { + if !config.styles.isEmpty { + RichTextStyle.ToggleGroup( + context: context, + styles: config.styles + ) + } + } // @ViewBuilder // func superscriptButtons( diff --git a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift new file mode 100644 index 0000000..52da09f --- /dev/null +++ b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift @@ -0,0 +1,239 @@ +// +// RichTextKeyboardToolbar.swift +// RichTextKit +// +// Created by Daniel Saidi on 2022-12-14. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. +// + +#if iOS || 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: View { + + /** + Create a rich text keyboard toolbar. + + - Parameters: + - context: The context to affect. + - leadingButtons: The leading buttons to place after the leading actions. + - 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 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() + 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() + } + } +} + +private extension View { + + @ViewBuilder + func prefersMediumSize() -> some View { + #if macOS + self + #else + if #available(iOS 16, *) { + self.presentationDetents([.medium]) + } else { + self + } + #endif + } +} + +private extension RichTextKeyboardToolbar { + + var isCompact: Bool { + horizontalSizeClass == .compact + } +} + +private extension RichTextKeyboardToolbar { + + var divider: some View { + Divider() + .frame(height: 25) + } + + @ViewBuilder + var leadingViews: some View { + RichTextAction.ButtonStack( + context: context, + actions: config.leadingActions, + spacing: style.itemSpacing + ) + + leadingButtons(StandardLeadingButtons()) + + divider + + Button(action: presentFormatSheet) { + Image.richTextFormat + .contentShape(Rectangle()) + } + + RichTextStyle.ToggleStack(context: context) + .keyboardShortcutsOnly(if: isCompact) + + RichTextFont.SizePickerStack(context: context) + .keyboardShortcutsOnly() + } + + @ViewBuilder + var trailingViews: some View { + RichTextAlignment.Picker(selection: $context.textAlignment) + .pickerStyle(.segmented) + .frame(maxWidth: 200) + .keyboardShortcutsOnly(if: isCompact) + + trailingButtons(StandardTrailingButtons()) + + RichTextAction.ButtonStack( + context: context, + actions: config.trailingActions, + spacing: style.itemSpacing + ) + } +} + +private extension View { + + @ViewBuilder + func keyboardShortcutsOnly( + if condition: Bool = true + ) -> some View { + if condition { + self.hidden() + .frame(width: 0) + } else { + self + } + } +} + +private extension RichTextKeyboardToolbar { + + var shouldDisplayToolbar: Bool { context.isEditingText || config.alwaysDisplayToolbar } +} + +private extension RichTextKeyboardToolbar { + + func presentFormatSheet() { + isFormatSheetPresented = true + } +} +#endif diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift new file mode 100644 index 0000000..7a2e640 --- /dev/null +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift @@ -0,0 +1,107 @@ +// +// RichTextStyle+Button.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 22/11/24. +// + +import SwiftUI + +public extension RichTextStyle { + + /** + This button can be used to toggle a ``RichTextStyle``. + + This view renders a plain `Button`, which means you can + use and configure with plain SwiftUI. + */ + struct Button: View { + + /** + Create a rich text style button. + + - Parameters: + - style: The style to toggle. + - 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: RichTextStyle, + value: Binding, + fillVertically: Bool = false + ) { + self.style = style + self.value = value + self.fillVertically = fillVertically + } + + /** + Create a rich text style button. + + - Parameters: + - style: The style to toggle. + - context: The context to affect. + - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. + */ + public init( + style: RichTextStyle, + context: RichEditorState, + fillVertically: Bool = false + ) { + self.init( + style: style, + value: context.binding(for: style), + fillVertically: fillVertically + ) + } + + private let style: RichTextStyle + 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) + } + } +} + +extension View { + + @ViewBuilder + func foreground(_ color: Color, if isOn: Bool) -> some View { + if isOn { + self.foregroundStyle(color) + } else { + self + } + } + + @ViewBuilder + func tint(_ color: Color, if isOn: Bool) -> some View { + if isOn { + self.tint(color) + } else { + self + } + } +} + +private extension RichTextStyle.Button { + + var isOn: Bool { + value.wrappedValue + } + + func toggle() { + value.wrappedValue.toggle() + } +} diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift new file mode 100644 index 0000000..ecf0a13 --- /dev/null +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift @@ -0,0 +1,88 @@ +// +// RichTextStyle+Toggle.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 22/11/24. +// + +import SwiftUI + +public extension RichTextStyle { + + /** + This toggle can be used to toggle a ``RichTextStyle``. + + This view renders a plain `Toggle`, which means you can + use and configure with plain SwiftUI. The one exception + is the tint color, which is set with a style. + */ + struct Toggle: View { + + /** + Create a rich text style toggle toggle. + + - Parameters: + - style: The style to toggle. + - 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: RichTextStyle, + value: Binding, + fillVertically: Bool = false + ) { + self.style = style + self.value = value + self.fillVertically = fillVertically + } + + /** + Create a rich text style toggle. + + - Parameters: + - style: The style to toggle. + - context: The context to affect. + - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. + */ + public init( + style: RichTextStyle, + context: RichEditorState, + fillVertically: Bool = false + ) { + self.init( + style: style, + value: context.binding(for: style), + fillVertically: fillVertically + ) + } + + private let style: RichTextStyle + private let value: Binding + private let fillVertically: Bool + + 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 extension RichTextStyle.Toggle { + + var isOn: Bool { + value.wrappedValue + } +} + diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift new file mode 100644 index 0000000..a4767b4 --- /dev/null +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift @@ -0,0 +1,80 @@ +// +// RichTextStyle+ToggleGroup.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 22/11/24. +// + +#if iOS || macOS || os(visionOS) +import SwiftUI + +public extension RichTextStyle { + + /** + This view can list ``RichTextStyle/Toggle``s for a list + of ``RichTextStyle`` values, in a bordered button group. + + Since this view uses multiple styles, it binds directly + to a ``RichTextContext`` instead of individual values. + + > Important: Since the `ControlGroup` doesn't highlight + buttons in iOS, we use a `ToggleStack` for iOS. + */ + struct ToggleGroup: View { + + /** + Create a rich text style toggle button group. + + - Parameters: + - context: The context to affect. + - 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 + } + + 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 + } + + @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 + } + } +} +#endif diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift new file mode 100644 index 0000000..191d04b --- /dev/null +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift @@ -0,0 +1,58 @@ +// +// RichTextStyle+ToggleStack.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 22/11/24. +// + +import SwiftUI + +public extension RichTextStyle { + + /** + This view can list ``RichTextStyle/Toggle``s for a list + of ``RichTextStyle`` values, in a horizontal stack. + + Since this view uses multiple styles, it binds directly + to a ``RichTextContext`` instead of individual values. + */ + struct ToggleStack: View { + + /** + Create a rich text style toggle button group. + + - Parameters: + - context: The context to affect. + - styles: The styles to list, by default ``RichTextStyle/all``. + - spacing: The spacing to apply to stack items, by default `5`. + */ + public init( + context: RichEditorState, + styles: [RichTextStyle] = .all, + spacing: Double = 5 + ) { + self._context = ObservedObject(wrappedValue: context) + self.styles = styles + self.spacing = spacing + } + + private let styles: [RichTextStyle] + private let spacing: Double + + @ObservedObject + private var context: RichEditorState + + public var body: some View { + HStack(spacing: spacing) { + ForEach(styles) { + RichTextStyle.Toggle( + style: $0, + context: context, + fillVertically: true + ) + } + } + .fixedSize(horizontal: false, vertical: true) + } + } +} diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift new file mode 100644 index 0000000..79e840a --- /dev/null +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift @@ -0,0 +1,136 @@ +// +// RichTextStyle.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 22/11/24. +// + +import SwiftUI + +public enum RichTextStyle: String, CaseIterable, Identifiable, RichTextLabelValue { + + case bold + case italic + case underline + case strikethrough +} + +public extension RichTextStyle { + + /// All available rich text styles. + static var all: [Self] { allCases } +} + +public extension Collection where Element == RichTextStyle { + + /// All available rich text styles. + static var all: [RichTextStyle] { RichTextStyle.allCases } +} + +public extension RichTextStyle { + + var id: String { rawValue } + + /// 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 + } + } + + /// The localized style title. + var title: String { + titleKey.text + } + + /// The localized style title key. + 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. + + - Parameters: + - traits: The symbolic traits to inspect. + - attributes: The rich text attributes to inspect. + */ + 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 extension Collection where Element == RichTextStyle { + + /// Check if the collection contains a certain style. + func hasStyle(_ style: RichTextStyle) -> Bool { + contains(style) + } + + /// Check if a certain style change should be applied. + func shouldAddOrRemove( + _ style: RichTextStyle, + _ newValue: Bool + ) -> Bool { + let shouldAdd = newValue && !hasStyle(style) + let shouldRemove = !newValue && hasStyle(style) + return shouldAdd || shouldRemove + } +} + +#if canImport(UIKit) +public extension RichTextStyle { + + /// The symbolic font traits for the style, if any. + var symbolicTraits: UIFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .traitBold + case .italic: .traitItalic + case .strikethrough: nil + case .underline: nil + } + } +} +#endif + +#if macOS +public extension RichTextStyle { + + /// The symbolic font traits for the trait, if any. + 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 + } + } +} diff --git a/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift b/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift new file mode 100644 index 0000000..1b27031 --- /dev/null +++ b/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift @@ -0,0 +1,31 @@ +// +// View+RichTextStyle.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 22/11/24. +// + +import SwiftUI + +public extension View { + + /** + Add a keyboard shortcut that toggles a certain style. + + This modifier only has effect on platforms that support + keyboard shortcuts. + */ + @ViewBuilder + func keyboardShortcut(for style: RichTextStyle) -> some View { + #if iOS || macOS || os(visionOS) + switch style { + case .bold: keyboardShortcut("b", modifiers: .command) + case .italic: keyboardShortcut("i", modifiers: .command) + case .strikethrough: self + case .underline: keyboardShortcut("u", modifiers: .command) + } + #else + self + #endif + } +} diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift index fc6489a..9c02cf1 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift @@ -10,12 +10,12 @@ import SwiftUI public extension RichEditorState { /// Get a binding for a certain style. -// func binding(for style: RichTextStyle) -> Binding { -// Binding( -// get: { Bool(self.hasStyle(style)) }, -// set: { self.setStyle(style, to: $0) } -// ) -// } + func binding(for style: RichTextStyle) -> Binding { + Binding( + get: { Bool(self.hasStyle(style)) }, + set: { [weak self]_ in self?.setStyle(style) } + ) + } /// Check whether or not the context has a certain style. func hasStyle(_ style: RichTextStyle) -> Bool { @@ -36,6 +36,10 @@ public extension RichEditorState { func toggleStyle(_ style: RichTextStyle) { setStyle(style, to: !hasStyle(style)) } + + func setStyle(_ style: RichTextStyle) { + toggleStyle(style: style.richTextSpanStyle) + } } extension RichEditorState { diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift index a5bd8d7..3c3c25e 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift @@ -116,7 +116,7 @@ public class RichEditorState: ObservableObject { internal var adapter: EditorAdapter = DefaultAdapter() // @Published internal var attributedString: NSMutableAttributedString - @Published internal var activeStyles: Set = [] + @Published internal var activeStyles: Set = [] @Published internal var activeAttributes: [NSAttributedString.Key: Any]? = [:] internal var currentFont: FontRepresentable = .systemFont(ofSize: .standardRichTextFontSize) diff --git a/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift index c4b9e66..98f22b9 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift @@ -39,9 +39,9 @@ extension RichEditorState { /** This will toggle the style - Parameters: - - style: is of type TextSpanStyle + - style: is of type RichTextSpanStyle */ - public func toggleStyle(style: TextSpanStyle) { + public func toggleStyle(style: RichTextSpanStyle) { if activeStyles.contains(style) { setInternalStyles(style: style, add: false) removeStyle(style) @@ -54,9 +54,9 @@ extension RichEditorState { /** This will update the style - Parameters: - - style: is of type TextSpanStyle + - style: is of type RichTextSpanStyle */ - public func updateStyle(style: TextSpanStyle) { + public func updateStyle(style: RichTextSpanStyle) { setInternalStyles(style: style) setStyle(style) } @@ -126,10 +126,10 @@ extension RichEditorState { /** Set the activeStyles - Parameters: - - style: is of type TextSpanStyle + - style: is of type RichTextSpanStyle This will set the activeStyle according to style passed */ - private func setStyle(_ style: TextSpanStyle) { + private func setStyle(_ style: RichTextSpanStyle) { activeStyles.removeAll() activeAttributes = [:] activeStyles.insert(style) @@ -144,7 +144,7 @@ extension RichEditorState { updateCurrentSpanStyle() } - func checkIfStyleIsActiveWithSameAttributes(_ style: TextSpanStyle) -> Bool { + func checkIfStyleIsActiveWithSameAttributes(_ style: RichTextSpanStyle) -> Bool { var addStyle: Bool = true switch style { case .size(let size): @@ -187,7 +187,7 @@ extension RichEditorState { */ internal func updateCurrentSpanStyle() { guard !attributedString.string.isEmpty else { return } - var newStyles: Set = [] + var newStyles: Set = [] if selectedRange.isCollapsed { newStyles = getRichSpanStyleByTextIndex(selectedRange.location - 1) @@ -211,10 +211,10 @@ extension RichEditorState { /** This will add style to the selected text - Parameters: - - style: which is of type TextSpanStyle + - style: which is of type RichTextSpanStyle It will add style to the selected text if needed and set activeAttributes and activeStyle accordingly. */ - private func addStyle(_ style: TextSpanStyle) { + private func addStyle(_ style: RichTextSpanStyle) { guard !activeStyles.contains(style) else { return } activeStyles.insert(style) @@ -228,7 +228,7 @@ extension RichEditorState { /** This will add style to the range of text - Parameters: - - style: which is of type TextSpanStyle + - style: which is of type RichTextSpanStyle - range: is the range of the text on which you want to apply the style */ private func applyStylesToSelectedText(_ spans: [RichTextSpanInternal]) { @@ -266,11 +266,11 @@ extension RichEditorState { /** This will remove style from active style if it contains it - Parameters: - - style: which is of type TextSpanStyle + - style: which is of type RichTextSpanStyle This will remove typing attributes as well for style. */ - private func removeStyle(_ style: TextSpanStyle) { + private func removeStyle(_ style: RichTextSpanStyle) { guard activeStyles.contains(style) || style.isDefault else { return } activeStyles.remove(style) updateTypingAttributes() @@ -298,7 +298,7 @@ extension RichEditorState { /** This will remove the attributes from text for style - Parameters: - - style: which is of type of TextSpanStyle + - style: which is of type of RichTextSpanStyle */ private func removeAttributes(_ spans: [RichTextSpanInternal]) { updateAttributes(spans: spans.map({ ($0, false) })) @@ -371,7 +371,7 @@ extension RichEditorState { This will update the span according to requirement, like break, remove, merge or extend. */ - private func processSpan(_ richTextSpan: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, selectedStyles: inout Set, forward: Bool = false) { + private func processSpan(_ richTextSpan: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, selectedStyles: inout Set, forward: Bool = false) { let newFromIndex = richTextSpan.from + typedChars let newToIndex = richTextSpan.to + typedChars @@ -390,7 +390,7 @@ extension RichEditorState { } } - func divideSpanAndAddTextWithCurrentStyle(span: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, with styles: inout Set) { + func divideSpanAndAddTextWithCurrentStyle(span: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, with styles: inout Set) { guard let index = internalSpans.firstIndex(of: span) else { return } let extendedSpan = span.copy(to: span.to + typedChars) @@ -504,9 +504,9 @@ extension RichEditorState { /** This will handle the adding header style in editor and to relative span - Parameters: - - style: is of type TextSpanStyle + - style: is of type RichTextSpanStyle */ - private func handleAddOrRemoveHeaderOrListStyle(in range: NSRange, style: TextSpanStyle, byAdding: Bool = true) { + private func handleAddOrRemoveHeaderOrListStyle(in range: NSRange, style: RichTextSpanStyle, byAdding: Bool = true) { guard !rawText.isEmpty else { return } let range = style.isList ? getListRangeFor(range, in: rawText) : rawText.getHeaderRangeFor(range) @@ -535,10 +535,10 @@ extension RichEditorState { /** This will create span for selected text with provided style - Parameters: - - styles: is of type [TextSpanStyle] + - styles: is of type [RichTextSpanStyle] - range: is of type NSRange */ - private func processSpansFor(new style: RichTextStyle, in range: NSRange, addStyle: Bool = true) { + private func processSpansFor(new style: RichTextSpanStyle, in range: NSRange, addStyle: Bool = true) { guard !range.isCollapsed else { return } @@ -593,7 +593,7 @@ extension RichEditorState { } } - private func processCompleteOverlappingSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { + private func processCompleteOverlappingSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextSpanStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { var processedSpans: [RichTextSpanInternal] = [] for span in spans { @@ -622,7 +622,7 @@ extension RichEditorState { return processedSpans } - private func processPartialOverlappingSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { + private func processPartialOverlappingSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextSpanStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { var processedSpans: [RichTextSpanInternal] = [] for span in spans { @@ -643,7 +643,7 @@ extension RichEditorState { return processedSpans } - private func processSameSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { + private func processSameSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextSpanStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { var processedSpans: [RichTextSpanInternal] = [] processedSpans = spans.map({ $0.copy(attributes: $0.attributes?.copy(with: style, byAdding: addStyle)) }) @@ -761,30 +761,32 @@ extension RichEditorState { } /** - This will provide Set of TextSpanStyle applied on given index + This will provide Set of RichTextSpanStyle applied on given index - Parameters: - index: index or location of text */ - private func getRichSpanStyleByTextIndex(_ index: Int) -> Set { + private func getRichSpanStyleByTextIndex(_ index: Int) -> Set { let styles = Set(internalSpans.filter { index >= $0.from && index <= $0.to }.map { $0.attributes?.styles() ?? []}.flatMap({ $0 })) return styles } /** - This will provide Array of TextSpanStyle applied on given range + This will provide Array of RichTextSpanStyle applied on given range - Parameters: - range: range of text which is of type NSRange */ - private func getRichSpanStyleListByTextRange(_ range: NSRange) -> [TextSpanStyle] { + private func getRichSpanStyleListByTextRange(_ range: NSRange) -> [RichTextSpanStyle] { return internalSpans.filter({ range.closedRange.overlaps($0.closedRange) }).map { $0.attributes?.styles() ?? [] }.flatMap({ $0 }) } } extension RichEditorState { - func setInternalStyles(style: RichTextStyle, add: Bool = true) { + func setInternalStyles(style: RichTextSpanStyle, add: Bool = true) { switch style { case .bold, .italic, .underline, .strikethrough: - setStyle(style, to: add) + if let style = style.richTextStyle { + setStyle(style, to: add) + } case .h1, .h2, .h3, .h4, .h5, .h6, .default: actionPublisher.send(.setHeaderStyle(style)) case .bullet(_): diff --git a/Sources/RichEditorSwiftUI/UI/Editor/TextSpanStyle.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift similarity index 93% rename from Sources/RichEditorSwiftUI/UI/Editor/TextSpanStyle.swift rename to Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift index 13eb854..b235786 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/TextSpanStyle.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift @@ -7,11 +7,9 @@ import SwiftUI -public typealias RichTextStyle = TextSpanStyle +public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { -public enum TextSpanStyle: Equatable, CaseIterable, Hashable { - - public static var allCases: [TextSpanStyle] = [ + public static var allCases: [RichTextSpanStyle] = [ .default, .bold, .italic, @@ -146,7 +144,7 @@ public enum TextSpanStyle: Equatable, CaseIterable, Hashable { } } - public static func == (lhs: TextSpanStyle, rhs: TextSpanStyle) -> Bool { + public static func == (lhs: RichTextSpanStyle, rhs: RichTextSpanStyle) -> Bool { return lhs.key == rhs.key } @@ -204,6 +202,16 @@ public enum TextSpanStyle: Equatable, CaseIterable, Hashable { } } + 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): @@ -332,7 +340,7 @@ public enum TextSpanStyle: Equatable, CaseIterable, Hashable { } #if canImport(UIKit) -public extension RichTextStyle { +public extension RichTextSpanStyle { /// The symbolic font traits for the style, if any. var symbolicTraits: UIFontDescriptor.SymbolicTraits? { @@ -346,7 +354,7 @@ public extension RichTextStyle { #endif #if macOS -public extension RichTextStyle { +public extension RichTextSpanStyle { /// The symbolic font traits for the trait, if any. var symbolicTraits: NSFontDescriptor.SymbolicTraits? { @@ -360,7 +368,7 @@ public extension RichTextStyle { #endif -extension TextSpanStyle { +extension RichTextSpanStyle { func getRichAttribute() -> RichAttributes? { switch self { case .default: @@ -400,7 +408,7 @@ extension TextSpanStyle { } -public extension Collection where Element == RichTextStyle { +public extension Collection where Element == RichTextSpanStyle { /** Check if the collection contains a certain style. @@ -408,13 +416,13 @@ public extension Collection where Element == RichTextStyle { - Parameters: - style: The style to look for. */ - func hasStyle(_ style: RichTextStyle) -> Bool { + func hasStyle(_ style: RichTextSpanStyle) -> Bool { contains(style) } /// Check if a certain style change should be applied. func shouldAddOrRemove( - _ style: RichTextStyle, + _ style: RichTextSpanStyle, _ newValue: Bool ) -> Bool { let shouldAdd = newValue && !hasStyle(style) diff --git a/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorTextStyleTool.swift b/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorTextStyleTool.swift index dceff0e..407439a 100644 --- a/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorTextStyleTool.swift +++ b/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorTextStyleTool.swift @@ -61,7 +61,7 @@ enum EditorTextStyleTool: CaseIterable, Hashable { } } - func getTextSpanStyle() -> TextSpanStyle { + func getTextSpanStyle() -> RichTextSpanStyle { switch self { case .header(let headerOptions): switch headerOptions { @@ -88,7 +88,7 @@ enum EditorTextStyleTool: CaseIterable, Hashable { } } - func isSelected(_ currentStyle: Set) -> Bool { + func isSelected(_ currentStyle: Set) -> Bool { switch self { case .header: return currentStyle.contains(.h1) || currentStyle.contains(.h2) || currentStyle.contains(.h3) || currentStyle.contains(.h4) || currentStyle.contains(.h5) || currentStyle.contains(.h6) diff --git a/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorToolBarView.swift b/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorToolBarView.swift index 7d815c3..7a65ce0 100644 --- a/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorToolBarView.swift +++ b/Sources/RichEditorSwiftUI/UI/EditorToolBar/EditorToolBarView.swift @@ -49,7 +49,6 @@ public struct EditorToolBarView: View { Section { RichTextFont.SizePickerStack(context: state) - } }) #if os(iOS) @@ -72,8 +71,8 @@ private struct ToggleStyleButton: View { @Environment(\.colorScheme) var colorScheme let tool: EditorTextStyleTool - let appliedTools: Set - let onToolSelect: (TextSpanStyle) -> Void + let appliedTools: Set + let onToolSelect: (RichTextSpanStyle) -> Void private var isSelected: Bool { tool.isSelected(appliedTools) @@ -113,8 +112,8 @@ struct TitleStyleButton: View { @Environment(\.colorScheme) var colorScheme let tool: EditorTextStyleTool - let appliedTools: Set - let setStyle: (TextSpanStyle) -> Void + let appliedTools: Set + let setStyle: (RichTextSpanStyle) -> Void private var isSelected: Bool { tool.isSelected(appliedTools) @@ -138,7 +137,7 @@ struct TitleStyleButton: View { } - func hasStyle(_ style: TextSpanStyle) -> Bool { + func hasStyle(_ style: RichTextSpanStyle) -> Bool { return appliedTools.contains(where: { $0.key == style.key }) } } diff --git a/Sources/RichEditorSwiftUI/UI/EditorToolBar/ListType.swift b/Sources/RichEditorSwiftUI/UI/EditorToolBar/ListType.swift index ddf3dc4..4c686fb 100644 --- a/Sources/RichEditorSwiftUI/UI/EditorToolBar/ListType.swift +++ b/Sources/RichEditorSwiftUI/UI/EditorToolBar/ListType.swift @@ -38,7 +38,7 @@ public enum ListType: Codable, Identifiable, CaseIterable, Hashable { } extension ListType { - func getTextSpanStyle() -> TextSpanStyle { + func getTextSpanStyle() -> RichTextSpanStyle { switch self { case .bullet(let indent): return .bullet(indent) diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift index a6af737..a59d602 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift @@ -184,8 +184,8 @@ extension FontRepresentable { public extension FontRepresentable { /// Get a new font by adding a text style. - func addFontStyle(_ style: TextSpanStyle) -> FontRepresentable { - guard let trait = style.symbolicTraits, !fontDescriptor.symbolicTraits.contains(trait) else { return self } + func addFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { + guard let style = style.richTextStyle, let trait = style.symbolicTraits, !fontDescriptor.symbolicTraits.contains(trait) else { return self } let fontDesc = fontDescriptor.byTogglingStyle(style) #if os(macOS) if let familyName { @@ -202,8 +202,8 @@ public extension FontRepresentable { } ///Get a new font by removing a text style. - func removeFontStyle(_ style: TextSpanStyle) -> FontRepresentable { - guard let trait = style.symbolicTraits, fontDescriptor.symbolicTraits.contains(trait) else { return self } + func removeFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { + guard let style = style.richTextStyle, let trait = style.symbolicTraits, fontDescriptor.symbolicTraits.contains(trait) else { return self } let fontDesc = fontDescriptor.byTogglingStyle(style) #if os(macOS) if let familyName { @@ -220,7 +220,8 @@ public extension FontRepresentable { } /// Get a new font by toggling a text style. - func byTogglingFontStyle(_ style: TextSpanStyle) -> FontRepresentable { + func byTogglingFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { + guard let style = style.richTextStyle else { return self } let fontDesc = fontDescriptor.byTogglingStyle(style) #if os(macOS) if let familyName { @@ -229,6 +230,7 @@ public extension FontRepresentable { return FontRepresentable( descriptor: fontDesc, size: pointSize + ) ?? self #else fontDesc.withFamily(familyName)