Skip to content

Commit

Permalink
Deprecate many attribute writer functions
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsaidi committed Feb 14, 2024
1 parent 439c78b commit f4b6ae8
Show file tree
Hide file tree
Showing 19 changed files with 238 additions and 109 deletions.
10 changes: 9 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ Until then, minor updates may remove deprecated features and introduce breaking

This release adds a lot of new `RichTextAction` cases and adjusts the context and coordinator subscription.

This release also deprecates `RichTextAttributeReader` and `RichTextAttributeWriter` functionality that are not used by the library, in favor of `RichTextViewComponent`. This is done to reduce the complexity of the library.

Until now, the functions mostly did the same things, but sometimes the `RichTextViewComponent` have to use its text storage or layout manager, or update the typing attributes. Since these are not present in the reader and writer protocols, the code diverged and mostly identical copies had to co-exist. And since the reader and writer versions are not used by the library, they may not work as well as intended.

This change will hopefully make it possible to simplify the library in the 1.0 release, and focus more on unifying the different TextView platform implementations (`UITextView` in iOS and `NSTextView` in macOS), rather than providing a low level string handling interface.

### ✨ Features

* `FontRepresentable` has new extensions.
* `RichTextKeyboardToolbar` has a new config to always be shown.
* `RichTextView` has a new theme that lets you define its style.
* `RichTextViewComponent` has a new `hasRichTextStyle` function.
Expand All @@ -35,7 +42,8 @@ This release adds a lot of new `RichTextAction` cases and adjusts the context an

### 🗑️ Deprecations

* `RichTextCoordinator` functions that just trigger `handle(_:)` have been deprecated.
* `RichTextAttributeWriter` deprecates many functions in favor of `RichTextViewComponent`.
* `RichTextCoordinator` functions that simply triggered `handle(_:)` have been deprecated.



Expand Down
11 changes: 10 additions & 1 deletion Sources/RichTextKit/Attributes/RichTextAttributeWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ import Foundation
This protocol extends ``RichTextWriter`` with functionality
for writing attributes to the ``RichTextWriter/richText`.
This protocol is implemented by `NSMutableAttributedString`
This protocol is implemented by `NSMutableAttributedString`,
as well as other types in the library.
Note that this protocol used to have a lot of functionality
for setting various attributes, styles, etc. However, since
``RichTextViewComponent`` needs to perform changes in other
ways, we ended up with duplicated code where the writer had
functions that may have worked, but weren't used within the
library. As such, the ``RichTextViewComponent`` will be the
primary component for modifying rich text, while the writer
functionality is removed. This will help to avoid confusion.
*/
public protocol RichTextAttributeWriter: RichTextWriter, RichTextAttributeReader {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// RichTextViewComponent+Color.swift
// RichTextViewComponent+Colors.swift
// RichTextKit
//
// Created by Daniel Saidi on 2022-05-30.
Expand Down Expand Up @@ -27,4 +27,15 @@ public extension RichTextViewComponent {
guard let attribute = color.attribute else { return }
setRichTextAttribute(attribute, to: val)
}

/// Set a certain rich text color at a certain range.
func setRichTextColor(
_ color: RichTextColor,
to val: ColorRepresentable,
at range: NSRange
) {
guard let attribute = color.attribute else { return }
if richTextColor(color, at: range) == val { return }
setRichTextAttribute(attribute, to: val, at: range)
}
}
102 changes: 66 additions & 36 deletions Sources/RichTextKit/Component/RichTextViewComponent+Font.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,25 @@
// RichTextKit
//
// Created by Daniel Saidi on 2022-05-29.
// Copyright © 2022-2023 Daniel Saidi. All rights reserved.
// Copyright © 2022-2024 Daniel Saidi. All rights reserved.
//

/// These functions may seem complicated, but it is the only
/// way that seems to work correctly, so far.
///
/// I previously grabbed the `typingAttributes` and took the
/// `.font` attribute from it, then took its `fontDescriptor`
/// and created a new font with `withFamily`, then created a
/// new font with the new descriptor and old size.
///
/// That approach however fails since the San Francisco font
/// specifies a certain usage, that casuses the font name to
/// not apply. This code just creates a new font instead, so
/// be aware if something doesn't work as expected.
///
/// After removing the ``RichTextAttributeWriter`` in 1.0 we
/// can hopefully iterate more consistently on the extension.

import CoreGraphics
import Foundation

Expand All @@ -25,7 +41,7 @@ public extension RichTextViewComponent {
func setRichTextFontName(_ name: String) {
if richTextFont?.fontName == name { return }
if hasSelectedRange {
setRichTextFontName(name, at: selectedRange)
setFontName(name, at: selectedRange)
} else {
setFontNameAtCurrentPosition(to: name)
}
Expand All @@ -35,11 +51,11 @@ public extension RichTextViewComponent {
func setRichTextFontSize(_ size: CGFloat) {
if size == richTextFont?.pointSize { return }
#if macOS
setRichTextFontSize(size, at: selectedRange)
setFontSize(size, at: selectedRange)
setFontSizeAtCurrentPosition(size)
#else
if hasSelectedRange {
setRichTextFontSize(size, at: selectedRange)
setFontSize(size, at: selectedRange)
} else {
setFontSizeAtCurrentPosition(size)
}
Expand All @@ -56,22 +72,7 @@ public extension RichTextViewComponent {

private extension RichTextViewComponent {

/**
Set the font at the current position.
This function may seem complicated, but so far it's the
only way setting the font name seems to work correctly.
I previously grabbed the `typingAttributes` and grabbed
the `[.font]` attribute from that dictionary, then took
its `fontDescriptor` and created the new font using the
`withFamily` function, then created a new font with the
new descriptor and the old font point size. However, it
fails, since the San Francisco font specifies a certain
usage that causes the font name to not apply. This code
just creates a new font, but be aware of this change if
something turns out not to work as expected.
*/
/// Set the font at the current position.
func setFontNameAtCurrentPosition(to name: String) {
var attributes = typingAttributes
let oldFont = attributes[.font] as? FontRepresentable ?? .standardRichTextFont
Expand All @@ -81,27 +82,56 @@ private extension RichTextViewComponent {
typingAttributes = attributes
}

/**
Set the font size at the current position.
This function may seem complicated, but so far it's the
only way setting the font name seems to work correctly.
I previously grabbed the `typingAttributes` and grabbed
the `[.font]` attribute from that dictionary, then took
its `fontDescriptor` and created the new font using the
`withFamily` function, then created a new font with the
new descriptor and the old font point size. However, it
fails, since the San Francisco font specifies a certain
usage that causes the font name to not apply. This code
just creates a new font, but be aware of this change if
something turns out not to work as expected.
*/
/// Set the font size at the current position.
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.
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.
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()
}
}

private extension RichTextAttributeWriter {

/// We must adjust empty font names on some platforms.
func settableFontName(for fontName: String) -> String {
#if macOS
fontName
#else
fontName
#endif
}
}
99 changes: 95 additions & 4 deletions Sources/RichTextKit/Component/RichTextViewComponent+Indent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,26 @@ public extension RichTextViewComponent {
}

/// Set the rich text indent at current range.
///
/// Unlike some other attributes, this attribute applies
/// to the entire paragraph, not just the selected range.
/// It therefore needs special treatment.
func stepRichTextIndent(
points: CGFloat
) {
if !hasTrimmedText { return step(points: points) }
if !hasTrimmedText { return stepIndent(points: points) }
let previousCharacter = richText.string.character(at: selectedRange.location - 1)
let isNewLine = previousCharacter?.isNewLineSeparator ?? false
if isNewLine { return step(points: points) }
typingAttributes = stepRichTextIndent(points: points, at: selectedRange) ?? typingAttributes
if isNewLine { return stepIndent(points: points) }
typingAttributes = stepIndent(points: points, at: selectedRange) ?? typingAttributes
}
}

private extension RichTextViewComponent {

func step(points: CGFloat) {
func stepIndent(
points: CGFloat
) {
guard let style = typingAttributes[.paragraphStyle] as? NSParagraphStyle else { return }
guard let mutableStyle = style.mutableCopy() as? NSMutableParagraphStyle else { return }

Expand All @@ -49,4 +55,89 @@ private extension RichTextViewComponent {
attributes[.paragraphStyle] = mutableStyle
typingAttributes = attributes
}

/// Set the rich text indent at a certain range.
func stepIndent(
points: CGFloat,
at range: NSRange
) -> RichTextAttributes? {
let text = richText.string

// Text view has selected text
if range.length > 0 {
return stepIndentInternal(points: points, at: range)
}

// The cursor is at the beginning of the text
if range.location == 0 {
return stepIndent(points: points, atIndex: 0)
}

// The cursor is immediately before a newline
if let char = text.character(at: range.location), char.isNewLineSeparator {
let location = UInt(range.location)
let index = text.findIndexOfCurrentParagraph(from: location)
return stepIndent(points: points, atIndex: index)
}

// The cursor is somewhere within a paragraph
let location = UInt(range.location)
let index = text.findIndexOfCurrentParagraph(from: location)
return stepIndent(points: points, atIndex: index)
}
}


private extension RichTextAttributeWriter {

/// Step the rich text indent at a certain index.
func stepIndent(
points: CGFloat,
atIndex index: Int
) -> RichTextAttributes? {
guard let text = mutableRichText else { return nil }
let range = NSRange(location: index, length: 1)
let safeRange = safeRange(for: range, isAttributeOperation: true)
var attributes = text.attributes(at: safeRange.location, effectiveRange: nil)
let style = attributes[.paragraphStyle] as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()

let newIndent = max(style.headIndent + points, 0)
style.firstLineHeadIndent = newIndent
style.headIndent = newIndent

attributes[.paragraphStyle] = style
text.beginEditing()
text.setAttributes(attributes, range: safeRange)
text.fixAttributes(in: safeRange)
text.endEditing()

return attributes
}

/// Step the rich text indent at a certain index.
func stepIndent(
points: CGFloat,
atIndex index: UInt
) -> RichTextAttributes? {
stepIndent(
points: points,
atIndex: Int(index)
)
}

/// Step the text indent at a certain range.
func stepIndentInternal(
points: CGFloat,
at range: NSRange
) -> RichTextAttributes? {
let text = richText.string
_ = range.length
let location = range.location
let ulocation = UInt(location)
let index = text.findIndexOfCurrentParagraph(from: ulocation)
return stepIndent(
points: points,
atIndex: index
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public extension RichTextViewComponent {
switch style {
case .bold, .italic:
let styles = richTextStyles
guard shouldAddOrRemove(style, newValue, given: styles) else { return }
guard styles.shouldAddOrRemove(style, newValue) else { return }
guard let font = richTextFont else { return }
guard let newFont = newFont(for: font, byToggling: style) else { return }
guard let newFont = font.toggling(style) else { return }
setRichTextFont(newFont)
case .underlined:
setRichTextAttribute(.underlineStyle, to: value)
Expand Down
1 change: 0 additions & 1 deletion Sources/RichTextKit/Context/RichTextContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ public extension RichTextContext {
/// Set the attributed string to a new rich text.
func setAttributedString(to string: NSAttributedString) {
let mutable = NSMutableAttributedString(attributedString: string)
mutable.setRichTextFontSize(fontSize, at: mutable.richTextRange)
userActionPublisher.send(.setAttributedString(mutable))
}

Expand Down
19 changes: 13 additions & 6 deletions Sources/RichTextKit/Fonts/FontRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ public typealias FontRepresentable = NSFont

public extension FontRepresentable {

/**
The standard font to use for rich text.
You can change this value to affect all types that make
use of the value.
*/
/// The standard font to use for rich text.
///
/// You can use this value to change the global default.
static var standardRichTextFont = systemFont(ofSize: .standardRichTextFontSize)

/// Create a new font by toggling a certain style.
func toggling(
_ style: RichTextStyle
) -> FontRepresentable? {
.init(
descriptor: fontDescriptor.byTogglingStyle(style),
size: pointSize
)
}
}
Loading

0 comments on commit f4b6ae8

Please sign in to comment.