Skip to content

Commit

Permalink
Default attributes (#34)
Browse files Browse the repository at this point in the history
* apply default text view attributes

* always use stored properties, even with TK2

* opt into TextKit 2 on iOS, too

Circumvents #20

* adjust tests for permanent attributes

* fix pasting code from Xcode rendering oddly

* store defaultTextViewAttributes to fix applying outdated styles
  • Loading branch information
DivineDominion authored Nov 18, 2023
1 parent cdbbe36 commit 33b5b69
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 28 deletions.
11 changes: 9 additions & 2 deletions Projects/NeonExample-iOS/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,32 @@ final class ViewController: UIViewController {
let boldFont = UIFont.monospacedSystemFont(ofSize: 16, weight: .bold)
let italicFont = regularFont.fontDescriptor.withSymbolicTraits(.traitItalic).map { UIFont(descriptor: $0, size: 16) } ?? regularFont

// Set the default styles. This is applied by stock `NSTextStorage`s during
// so-called "attribute fixing" when you type, and we emulate that as
// part of the highlighting process in `TextViewSystemInterface`.
textView.font = regularFont
textView.textColor = .darkGray

let provider: TextViewSystemInterface.AttributeProvider = { token in
return switch token.name {
case let keyword where keyword.hasPrefix("keyword"): [.foregroundColor: UIColor.red, .font: boldFont]
case "comment": [.foregroundColor: UIColor.green, .font: italicFont]
default: [.foregroundColor: UIColor.darkText, .font: regularFont]
// Note: Default is not actually applied to unstyled/untokenized text.
default: [.foregroundColor: UIColor.blue, .font: regularFont]
}
}

return try! TextViewHighlighter(textView: textView,
language: language,
highlightQuery: query,
executionMode: .synchronous,
attributeProvider: provider)
}()

override func viewDidLoad() {
super.viewDidLoad()

_ = highlighter.textView
_ = textView.layoutManager

textView.text = """
// Example Code!
Expand Down
10 changes: 7 additions & 3 deletions Projects/NeonExample/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@ final class ViewController: NSViewController {

init() {
self.textView = NSTextView()
textView.isRichText = false // Discards any attributes when pasting.

scrollView.documentView = textView

let regularFont = NSFont.monospacedSystemFont(ofSize: 16, weight: .regular)
let boldFont = NSFont.monospacedSystemFont(ofSize: 16, weight: .bold)
let italicFont = NSFont(descriptor: regularFont.fontDescriptor.withSymbolicTraits(.italic), size: 16) ?? regularFont

// Alternatively, set `textView.typingAttributes = [.font: regularFont, ...]`
// if you want to customize other default (fallback) attributes.
// Set the default styles. This is applied by stock `NSTextStorage`s during
// so-called "attribute fixing" when you type, and we emulate that as
// part of the highlighting process in `TextViewSystemInterface`.
textView.font = regularFont
textView.textColor = .darkGray

let provider: TextViewSystemInterface.AttributeProvider = { token in
return switch token.name {
case let keyword where keyword.hasPrefix("keyword"): [.foregroundColor: NSColor.red, .font: boldFont]
case "comment": [.foregroundColor: NSColor.green, .font: italicFont]
default: [.foregroundColor: NSColor.textColor, .font: regularFont]
// Note: Default is not actually applied to unstyled/untokenized text.
default: [.foregroundColor: NSColor.blue, .font: regularFont]
}
}

Expand Down
35 changes: 20 additions & 15 deletions Sources/Neon/TextViewSystemInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@ public struct TextViewSystemInterface {

public let textView: TextView
public let attributeProvider: AttributeProvider
public var defaultTextViewAttributes: [NSAttributedString.Key: Any] = [:]

public init(textView: TextView, attributeProvider: @escaping AttributeProvider) {
public init(
textView: TextView,
defaultTextViewAttributes: [NSAttributedString.Key: Any] = [:],
attributeProvider: @escaping AttributeProvider
) {
self.textView = textView
// Assume that the default styles used before enabling any highlighting
// should be retained, unless client code overrides this.
self.defaultTextViewAttributes = [
.font: textView.font as Any,
.foregroundColor: textView.textColor as Any,
].merging(defaultTextViewAttributes) { _, override in override }
self.attributeProvider = attributeProvider
}

Expand All @@ -43,34 +54,28 @@ public struct TextViewSystemInterface {
}

extension TextViewSystemInterface: TextSystemInterface {
private func setAttributes(_ attrs: [NSAttributedString.Key : Any], in range: NSRange) {
private func clamped(range: NSRange) -> NSRange {
let endLocation = min(range.max, length)

assert(endLocation == range.max, "range is out of bounds, is the text state being updated correctly?")

let clampedRange = NSRange(range.location..<endLocation)
return NSRange(range.location..<endLocation)
}

// try text kit 2 first
if
#available(macOS 12, iOS 15.0, tvOS 15.0, *),
let textLayoutManager = textLayoutManager,
let contentManager = textLayoutManager.textContentManager,
let textRange = NSTextRange(clampedRange, provider: contentManager)
{
textLayoutManager.setRenderingAttributes(attrs, for: textRange)
return
}
private func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, in range: NSRange) {
let clampedRange = clamped(range: range)

// For TextKit 1: Fall back to applying styles directly to the storage.
// Both `NSTextLayoutManager.setRenderingAttributes` and
// `NSLayoutManager.setTemporaryAttributes` is limited to attributes
// that don't affect layout, like color. So it ignores fonts,
// making font weight changes or italicizing text impossible.
assert(textStorage != nil, "TextView's NSTextStorage cannot be nil")
let attrs = attrs ?? defaultTextViewAttributes
textStorage?.setAttributes(attrs, range: clampedRange)
}

public func clearStyle(in range: NSRange) {
setAttributes([:], in: range)
setAttributes(nil, in: range)
}

public func applyStyle(to token: Token) {
Expand Down
13 changes: 5 additions & 8 deletions Tests/NeonTests/TextViewSystemInterfaceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ final class TextViewSystemInterfaceTests: XCTestCase {
XCTAssertEqual(attrs.count, 1)
XCTAssertEqual(attrs[.foregroundColor] as? PlatformColor, PlatformColor.red)
XCTAssertEqual(effectiveRange, NSRange(0..<6))

}
#endif

Expand All @@ -91,15 +90,13 @@ final class TextViewSystemInterfaceTests: XCTestCase {

system.applyStyle(to: Token(name: "test", range: NSRange(0..<6)))

let documentRange = textLayoutManager.documentRange

var attrRangePairs = [([NSAttributedString.Key: Any], NSTextRange)]()
let textStorage = try XCTUnwrap(system.textStorage)
let documentRange = NSRange(location: 0, length: textStorage.length)

textLayoutManager.enumerateRenderingAttributes(from: documentRange.location, reverse: false, using: { _, attrs, range in
var attrRangePairs = [([NSAttributedString.Key: Any], NSRange)]()
textStorage.enumerateAttributes(in: documentRange) { attrs, range, _ in
attrRangePairs.append((attrs, range))

return true
})
}

XCTAssertEqual(attrRangePairs.count, 1)

Expand Down

0 comments on commit 33b5b69

Please sign in to comment.