Skip to content

Commit 24576b6

Browse files
authored
Support text alignment (#59)
1 parent 9e4cbdf commit 24576b6

12 files changed

+286
-22
lines changed

Sources/RichEditorSwiftUI/Alignment/RichTextAlignment.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public enum RichTextAlignment: String, CaseIterable, Codable, Equatable, Identif
2424
case .left: self = .left
2525
case .right: self = .right
2626
case .center: self = .center
27-
case .justified: self = .justified
27+
case .justified: self = .justify
2828
default: self = .left
2929
}
3030
}
@@ -36,7 +36,7 @@ public enum RichTextAlignment: String, CaseIterable, Codable, Equatable, Identif
3636
case center
3737

3838
/// Justified text alignment.
39-
case justified
39+
case justify
4040

4141
/// Right text alignment.
4242
case right
@@ -67,7 +67,7 @@ public extension RichTextAlignment {
6767
case .left: .left
6868
case .right: .right
6969
case .center: .center
70-
case .justified: .justified
70+
case .justify: .justified
7171
}
7272
}
7373
}

Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ extension RichTextCoordinator {
3131
syncContextWithTextView()
3232
case .selectRange(let range):
3333
setSelectedRange(to: range)
34-
case .setAlignment(_):
35-
//// textView.setRichTextAlignment(alignment)
36-
return
34+
case .setAlignment(let alignment):
35+
textView.setRichTextAlignment(alignment)
3736
case .setAttributedString(let string):
3837
setAttributedString(to: string)
3938
case .setColor(let color, let newValue):
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// RichTextViewComponent+Alignment.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 25/11/24.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(UIKit)
11+
import UIKit
12+
#elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
13+
import AppKit
14+
#endif
15+
16+
public extension RichTextViewComponent {
17+
18+
/// Get the text alignment.
19+
var richTextAlignment: RichTextAlignment? {
20+
guard let style = richTextParagraphStyle else { return nil }
21+
return RichTextAlignment(style.alignment)
22+
}
23+
24+
/// Set the text alignment.
25+
func setRichTextAlignment(_ alignment: RichTextAlignment) {
26+
if richTextAlignment == alignment { return }
27+
let style = NSMutableParagraphStyle(
28+
from: richTextParagraphStyle,
29+
alignment: alignment
30+
)
31+
setRichTextParagraphStyle(style)
32+
}
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// RichTextViewComponent+Paragraph.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 25/11/24.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(UIKit)
11+
import UIKit
12+
#endif
13+
14+
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
15+
import AppKit
16+
#endif
17+
18+
public extension RichTextViewComponent {
19+
20+
/// Get the paragraph style.
21+
var richTextParagraphStyle: NSMutableParagraphStyle? {
22+
richTextAttribute(.paragraphStyle)
23+
}
24+
25+
/// Set the paragraph style.
26+
///
27+
/// > Todo: The function currently can't handle multiple
28+
/// selected paragraphs. If many paragraphs are selected,
29+
/// it will only affect the first one.
30+
func setRichTextParagraphStyle(_ style: NSParagraphStyle) {
31+
let range = lineRange(for: selectedRange)
32+
guard range.length > 0 else { return }
33+
#if os(watchOS)
34+
setRichTextAttribute(.paragraphStyle, to: style, at: range)
35+
#else
36+
textStorageWrapper?.addAttribute(.paragraphStyle, value: style, range: range)
37+
#endif
38+
}
39+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// RichTextViewComponent+Ranges.swift
3+
// RichTextKit
4+
//
5+
// Created by Dominik Bucher
6+
//
7+
8+
import Foundation
9+
10+
extension RichTextViewComponent {
11+
12+
var notFoundRange: NSRange {
13+
.init(location: NSNotFound, length: 0)
14+
}
15+
16+
/// Get the line range at a certain text location.
17+
func lineRange(at location: Int) -> NSRange {
18+
#if os(watchOS)
19+
return notFoundRange
20+
#else
21+
guard
22+
let manager = layoutManagerWrapper,
23+
let storage = textStorageWrapper
24+
else { return NSRange(location: NSNotFound, length: 0) }
25+
let string = storage.string as NSString
26+
let locationRange = NSRange(location: location, length: 0)
27+
let lineRange = string.lineRange(for: locationRange)
28+
return manager.characterRange(forGlyphRange: lineRange, actualGlyphRange: nil)
29+
#endif
30+
}
31+
32+
/// Get the line range for a certain text range.
33+
func lineRange(for range: NSRange) -> NSRange {
34+
#if os(watchOS)
35+
return notFoundRange
36+
#else
37+
// Use the location-based logic if range is empty
38+
if range.length == 0 {
39+
return lineRange(at: range.location)
40+
}
41+
42+
guard let manager = layoutManagerWrapper else {
43+
return NSRange(location: NSNotFound, length: 0)
44+
}
45+
46+
var lineRange = NSRange(location: NSNotFound, length: 0)
47+
manager.enumerateLineFragments(
48+
forGlyphRange: range
49+
) { (_, _, _, glyphRange, stop) in
50+
lineRange = glyphRange
51+
stop.pointee = true
52+
}
53+
54+
// Convert glyph range to character range
55+
return manager.characterRange(forGlyphRange: lineRange, actualGlyphRange: nil)
56+
#endif
57+
}
58+
}

Sources/RichEditorSwiftUI/Data/Models/RichAttributes+RichTextAttributes.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import UIKit
1111
import AppKit
1212
#endif
1313

14+
import SwiftUI
15+
1416
extension RichAttributes {
1517
func toAttributes(font: FontRepresentable? = nil) -> RichTextAttributes {
1618
var attributes: RichTextAttributes = [:]
@@ -24,6 +26,14 @@ extension RichAttributes {
2426
)
2527
}
2628

29+
if let size = size {
30+
font = font.updateFontSize(size: CGFloat(size))
31+
}
32+
33+
if let fontName = self.font {
34+
font = font.updateFontName(with: fontName)
35+
}
36+
2737
// Apply bold and italic styles
2838
if let isBold = bold, isBold {
2939
font = font.makeBold()
@@ -45,6 +55,19 @@ extension RichAttributes {
4555
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
4656
}
4757

58+
if let color {
59+
attributes[.foregroundColor] = ColorRepresentable(Color(hex: color))
60+
}
61+
62+
if let background {
63+
attributes[.backgroundColor] = ColorRepresentable(Color(hex: background))
64+
}
65+
66+
if let align {
67+
let style = NSMutableParagraphStyle(from: nil, alignment: align)
68+
attributes[.paragraphStyle] = style
69+
}
70+
4871
// Handle indent and paragraph styles
4972
// if let indentLevel = indent {
5073
// let paragraphStyle = NSMutableParagraphStyle()

Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public struct RichAttributes: Codable {
2121
public let font: String?
2222
public let color: String?
2323
public let background: String?
24+
public let align: RichTextAlignment?
2425

2526
public init(
2627
// id: String = UUID().uuidString,
@@ -34,7 +35,8 @@ public struct RichAttributes: Codable {
3435
size: Int? = nil,
3536
font: String? = nil,
3637
color: String? = nil,
37-
background: String? = nil
38+
background: String? = nil,
39+
align: RichTextAlignment? = nil
3840
) {
3941
// self.id = id
4042
self.bold = bold
@@ -48,6 +50,7 @@ public struct RichAttributes: Codable {
4850
self.font = font
4951
self.color = color
5052
self.background = background
53+
self.align = align
5154
}
5255

5356
enum CodingKeys: String, CodingKey {
@@ -62,6 +65,7 @@ public struct RichAttributes: Codable {
6265
case font = "font"
6366
case color = "color"
6467
case background = "background"
68+
case align = "align"
6569
}
6670

6771
public init(from decoder: Decoder) throws {
@@ -78,6 +82,7 @@ public struct RichAttributes: Codable {
7882
self.font = try values.decodeIfPresent(String.self, forKey: .font)
7983
self.color = try values.decodeIfPresent(String.self, forKey: .color)
8084
self.background = try values.decodeIfPresent(String.self, forKey: .background)
85+
self.align = try values.decodeIfPresent(RichTextAlignment.self, forKey: .align)
8186
}
8287
}
8388

@@ -95,6 +100,7 @@ extension RichAttributes: Hashable {
95100
hasher.combine(font)
96101
hasher.combine(color)
97102
hasher.combine(background)
103+
hasher.combine(align)
98104
}
99105
}
100106

@@ -114,6 +120,7 @@ extension RichAttributes: Equatable {
114120
&& lhs.font == rhs.font
115121
&& lhs.color == rhs.color
116122
&& lhs.background == rhs.background
123+
&& lhs.align == rhs.align
117124
)
118125
}
119126
}
@@ -129,7 +136,8 @@ extension RichAttributes {
129136
size: Int? = nil,
130137
font: String? = nil,
131138
color: String? = nil,
132-
background: String? = nil
139+
background: String? = nil,
140+
align: RichTextAlignment? = nil
133141
) -> RichAttributes {
134142
return RichAttributes(
135143
bold: (bold != nil ? bold! : self.bold),
@@ -142,7 +150,8 @@ extension RichAttributes {
142150
size: (size != nil ? size! : self.size),
143151
font: (font != nil ? font! : self.font),
144152
color: (color != nil ? color! : self.color),
145-
background: (background != nil ? background! : self.background)
153+
background: (background != nil ? background! : self.background),
154+
align: (align != nil ? align! : self.align)
146155
)
147156
}
148157

@@ -163,7 +172,8 @@ extension RichAttributes {
163172
size: (att.size != nil ? (byAdding ? att.size! : nil) : self.size),
164173
font: (att.font != nil ? (byAdding ? att.font! : nil) : self.font),
165174
color: (att.color != nil ? (byAdding ? att.color! : nil) : self.color),
166-
background: (att.background != nil ? (byAdding ? att.background! : nil) : self.background)
175+
background: (att.background != nil ? (byAdding ? att.background! : nil) : self.background),
176+
align: (att.align != nil ? (byAdding ? att.align! : nil) : self.align)
167177
)
168178
}
169179
}
@@ -201,6 +211,9 @@ extension RichAttributes {
201211
if let background = background {
202212
styles.append(.background(.init(hex: background)))
203213
}
214+
if let align = align {
215+
styles.append(.align(align))
216+
}
204217
return styles
205218
}
206219

@@ -236,6 +249,9 @@ extension RichAttributes {
236249
if let background = background {
237250
styles.insert(.background(Color(hex: background)))
238251
}
252+
if let align = align {
253+
styles.insert(.align(align))
254+
}
239255
return styles
240256
}
241257
}
@@ -275,6 +291,8 @@ extension RichAttributes {
275291
return color == colorItem?.hexString
276292
case .background(let color):
277293
return background == color?.hexString
294+
case .align(let alignment):
295+
return align == alignment
278296
}
279297
}
280298
}
@@ -296,6 +314,7 @@ internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttribute
296314
var font: String? = nil
297315
var color: String? = nil
298316
var background: String? = nil
317+
var align: RichTextAlignment? = nil
299318

300319
for style in styles {
301320
switch style {
@@ -332,6 +351,8 @@ internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttribute
332351
color = textColor?.hexString
333352
case .background(let backgroundColor):
334353
background = backgroundColor?.hexString
354+
case .align(let alignment):
355+
align = alignment
335356
}
336357
}
337358
return RichAttributes(bold: bold,
@@ -344,6 +365,7 @@ internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttribute
344365
size: size,
345366
font: font,
346367
color: color,
347-
background: background
368+
background: background,
369+
align: align
348370
)
349371
}

Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,21 @@ public extension RichTextFormat {
7171

7272
Divider()
7373

74-
// SidebarSection {
75-
// alignmentPicker(value: $context.textAlignment)
74+
SidebarSection {
75+
alignmentPicker(value: $context.textAlignment)
76+
.onChangeBackPort(of: context.textAlignment) { newValue in
77+
context.updateStyle(style: .align(newValue))
78+
}
7679
// HStack {
7780
// lineSpacingPicker(for: context)
7881
// }
7982
// HStack {
8083
// indentButtons(for: context, greedy: true)
8184
// superscriptButtons(for: context, greedy: true)
8285
// }
83-
// }
84-
//
85-
// Divider()
86+
}
87+
88+
Divider()
8689

8790
if hasColorPickers {
8891
SidebarSection {

0 commit comments

Comments
 (0)